├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── improvement-suggestion.md ├── SUPPORT.md ├── dependabot.yml └── workflows │ ├── check.yml │ ├── linkcheck.yml │ └── test.yml ├── .gitignore ├── .readthedocs.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── CREDITS ├── ChangeLog ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── Makefile ├── _static │ └── .keep_dir ├── changelog.rst ├── conf.py ├── credits.rst ├── examples.rst ├── fuzzy.rst ├── ideas.rst ├── index.rst ├── internals.rst ├── introduction.rst ├── logo.png ├── logo.svg ├── make.bat ├── orms.rst ├── recipes.rst ├── reference.rst └── spelling_wordlist.txt ├── examples ├── Makefile ├── django_demo │ ├── django_demo │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── generic_foreignkey │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── factories.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ └── tests.py │ ├── manage.py │ ├── requirements.txt │ └── runtests.sh ├── flask_alchemy │ ├── demoapp.py │ ├── demoapp_factories.py │ ├── requirements.txt │ ├── runtests.sh │ └── test_demoapp.py └── requirements.txt ├── factory ├── __init__.py ├── alchemy.py ├── base.py ├── builder.py ├── declarations.py ├── django.py ├── enums.py ├── errors.py ├── faker.py ├── fuzzy.py ├── helpers.py ├── mogo.py ├── mongoengine.py ├── py.typed ├── random.py └── utils.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── alchemyapp │ ├── __init__.py │ └── models.py ├── alter_time.py ├── cyclic │ ├── __init__.py │ ├── bar.py │ ├── foo.py │ └── self_ref.py ├── djapp │ ├── __init__.py │ ├── models.py │ └── settings.py ├── test_alchemy.py ├── test_base.py ├── test_declarations.py ├── test_dev_experience.py ├── test_django.py ├── test_docs_internals.py ├── test_faker.py ├── test_fuzzy.py ├── test_helpers.py ├── test_mongoengine.py ├── test_regression.py ├── test_transformer.py ├── test_typing.py ├── test_using.py ├── test_utils.py ├── test_version.py ├── testdata │ ├── __init__.py │ ├── example.data │ └── example.jpeg └── utils.py └── tox.ini /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | #### Description 8 | *A clear and concise description of what the bug is.* 9 | 10 | #### To Reproduce 11 | *Share how the bug happened:* 12 | 13 | ##### Model / Factory code 14 | ```python 15 | # Include your factories and models here 16 | ``` 17 | 18 | ##### The issue 19 | *Add a short description along with your code* 20 | 21 | ```python 22 | # Include the code that provoked the bug, including as full a stack-trace as possible 23 | ``` 24 | 25 | #### Notes 26 | *Add any notes you feel relevant here :)* 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/improvement-suggestion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Improvement suggestion 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | #### The problem 8 | *Please describe the problem you're encountering (e.g "It's very complex to do [...]")* 9 | 10 | #### Proposed solution 11 | *Please provide some wild idea you think could solve this issue. It's much easier to work from an existing suggestion :)* 12 | 13 | #### Extra notes 14 | *Any notes you feel interesting to include: alternatives you've considered, reasons to include the change, anything!* 15 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Getting support 2 | 3 | Most questions should be asked with the `factory-boy` tag on 4 | [StackOverflow](https://stackoverflow.com/questions/tagged/factory-boy). 5 | Alternatively, a discussion group exists at 6 | https://groups.google.com/d/forum/factoryboy. 7 | 8 | Please **do not open issues for support requests**. Issues are meant for bug 9 | reports and improvement suggestions. 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | groups: 8 | GitHub_Actions: 9 | patterns: 10 | - "*" # Group all Actions updates into a single larger pull request 11 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | name: ${{ matrix.tox-environment }} 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | tox-environment: 22 | - docs 23 | - examples 24 | - lint 25 | 26 | env: 27 | TOXENV: ${{ matrix.tox-environment }} 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Set up Python 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: '3' 36 | cache: pip 37 | 38 | - name: Install dependencies 39 | run: python -m pip install tox 40 | 41 | - name: Run 42 | run: tox 43 | -------------------------------------------------------------------------------- /.github/workflows/linkcheck.yml: -------------------------------------------------------------------------------- 1 | name: Linkcheck 2 | 3 | on: 4 | schedule: 5 | - cron: '11 11 * * 1' 6 | 7 | jobs: 8 | build: 9 | name: Linkcheck 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: '3' 18 | 19 | - name: Install dependencies 20 | run: python -m pip install tox 21 | 22 | - name: Run linkcheck 23 | run: tox -e linkcheck 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | tests: 15 | name: Python ${{ matrix.python-version }} 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | python-version: 22 | - "3.9" 23 | - "3.10" 24 | - "3.11" 25 | - "3.12" 26 | - "3.13" 27 | - "pypy-3.10" 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Set up Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | cache: pip 37 | 38 | - name: Install dependencies 39 | run: python -m pip install tox-gh-actions 40 | 41 | - name: Run tests 42 | run: tox 43 | env: 44 | DATABASE_TYPE: ${{ matrix.database-type }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary files 2 | .*.swp 3 | *.pyc 4 | *.pyo 5 | .idea/ 6 | 7 | # Build-related files 8 | docs/_build/ 9 | auto_dev_requirements*.txt 10 | .coverage 11 | .tox 12 | *.egg-info 13 | *.egg 14 | build/ 15 | dist/ 16 | htmlcov/ 17 | MANIFEST 18 | tags 19 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: "ubuntu-lts-latest" 4 | tools: 5 | python: "latest" 6 | 7 | python: 8 | install: 9 | - method: pip 10 | path: . 11 | extra_requirements: 12 | - doc 13 | 14 | sphinx: 15 | configuration: docs/conf.py 16 | fail_on_warning: true 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at raphael DOT barrois AT xelmail DOT com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Thanks for taking the time to contribute to factory_boy! 5 | 6 | Code of Conduct 7 | --------------- 8 | 9 | This project and everyone participating in it is governed by the `Code of 10 | Conduct`_. By participating, you are expected to uphold this code. Please 11 | report inappropriate behavior to raphael DOT barrois AT xelmail DOT com. 12 | 13 | .. _Code of Conduct: https://github.com/FactoryBoy/factory_boy/blob/master/CODE_OF_CONDUCT.md 14 | 15 | *(If I'm the person with the inappropriate behavior, please accept my 16 | apologies. I know I can mess up. I can't expect you to tell me, but if you 17 | chose to do so, I'll do my best to handle criticism constructively. 18 | -- Raphaël)* 19 | 20 | *(As the community around this project grows, we hope to have more core 21 | developers available to handle that kind of issues)* 22 | 23 | 24 | Contributions 25 | ------------- 26 | 27 | Bug reports, patches, documentation improvements and suggestions are welcome! 28 | 29 | Please open an issue_ or send a `pull request`_. 30 | 31 | Feedback about the documentation is especially valuable — the authors of 32 | ``factory_boy`` feel more confident about writing code than writing docs :-) 33 | 34 | .. _issue: https://github.com/FactoryBoy/factory_boy/issues/new 35 | .. _pull request: https://github.com/FactoryBoy/factory_boy/compare/ 36 | 37 | 38 | Where to start? 39 | --------------- 40 | 41 | If you're new to the project and want to help, a great first step would be: 42 | 43 | * Fixing an issue in the docs (outdated setup instructions, missing information, 44 | unclear feature, etc.); 45 | * Working on an existing issue (some should be marked ``BeginnerFriendly``); 46 | * Reviewing an existing pull request; 47 | * Or any other way you'd like to help. 48 | 49 | 50 | Code contributions 51 | ------------------ 52 | 53 | In order to merge some code, you'll need to open a `pull request`_. 54 | 55 | There are a few rules to keep in mind regarding pull requests: 56 | 57 | * A pull request should only solve a single issue / add a single feature; 58 | * If the code change is significant, please also create an issue_ for easier discussion; 59 | * We have automated testing; please make sure that the updated code passes automated checks; 60 | * We're striving to improve the quality of the library, with higher test and docs coverage. 61 | If you don't know how/where to add docs or tests, we'll be very happy to point you in the right 62 | direction! 63 | 64 | 65 | Questions 66 | --------- 67 | 68 | GitHub issues aren't a good medium for handling questions. There are better 69 | places to ask questions, for example Stack Overflow; please use the 70 | ``factory-boy`` tag to make those questions easy to find by the maintainers. 71 | 72 | If you want to ask a question anyway, please make sure that: 73 | 74 | - it's a question about ``factory_boy`` and not about ``Django`` or ``Faker``; 75 | - it isn't answered by the documentation; 76 | - it wasn't asked already. 77 | 78 | A good question can be written as a suggestion to improve the documentation. 79 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | Credits 2 | ======= 3 | 4 | 5 | Maintainers 6 | ----------- 7 | 8 | The ``factory_boy`` project is operated and maintained by: 9 | 10 | * Jeff Widman (https://github.com/jeffwidman) 11 | * Raphaël Barrois (https://github.com/rbarrois) 12 | 13 | 14 | .. _contributors: 15 | 16 | Contributors 17 | ------------ 18 | 19 | The project was initially created by Mark Sandstrom . 20 | 21 | 22 | The project has received contributions from (in alphabetical order): 23 | 24 | * Adam Chainz 25 | * Alejandro 26 | * Alexey Kotlyarov 27 | * Amit Shah 28 | * Anas Zahim (https://github.com/kamotos) 29 | * Andrey Voronov 30 | * Branko Majic 31 | * Carl Meyer 32 | * Chris Lasher 33 | * Chris Seto 34 | * Christoph Sieghart 35 | * David Baumgold 36 | * Demur Nodia (https://github.com/demonno) 37 | * Eduard Iskandarov 38 | * Federico Bond (https://github.com/federicobond) 39 | * Flavio Curella 40 | * François Freitag 41 | * George Hickman 42 | * Grégoire Deveaux 43 | * Hervé Cauwelier 44 | * Hugo Osvaldo Barrera 45 | * Ilya Baryshev 46 | * Ilya Pirogov 47 | * Ionuț Arțăriși 48 | * Issa Jubril 49 | * Ivan Miric 50 | * Janusz Skonieczny 51 | * Javier Buzzi (https://github.com/kingbuzzman) 52 | * Jeff Widman (https://github.com/jeffwidman) 53 | * Jon Dufresne 54 | * Jonathan Tushman 55 | * Joshua Carp 56 | * Leonardo Lazzaro 57 | * Luke GB 58 | * Marc Abramowitz 59 | * Mark Sandstrom 60 | * Martin Bächtold (https://github.com/mbaechtold) 61 | * Michael Joseph 62 | * Mikhail Korobov 63 | * Oleg Pidsadnyi 64 | * Omer 65 | * Pauly Fenwar 66 | * Peter Marsh 67 | * Puneeth Chaganti 68 | * QuantumGhost 69 | * Raphaël Barrois (https://github.com/rbarrois) 70 | * Rich Rauenzahn 71 | * Richard Moch 72 | * Rob Zyskowski 73 | * Robrecht De Rouck 74 | * Samuel Paccoud 75 | * Sarah Boyce 76 | * Saul Shanabrook 77 | * Sean Löfgren 78 | * Shahriar Tajbakhsh 79 | * Tom 80 | * alex-netquity 81 | * anentropic 82 | * minimumserious 83 | * mluszczyk 84 | * nkryptic 85 | * obiwanus 86 | * tsouvarev 87 | * yamaneko 88 | 89 | 90 | 91 | Contributor license agreement 92 | ----------------------------- 93 | 94 | .. note:: This agreement is required to allow redistribution of submitted contributions. 95 | See http://oss-watch.ac.uk/resources/cla for an explanation. 96 | 97 | Any contributor proposing updates to the code or documentation of this project *MUST* 98 | add its name to the list in the :ref:`contributors` section, thereby "signing" the 99 | following contributor license agreement: 100 | 101 | They accept and agree to the following terms for their present end future contributions 102 | submitted to the ``factory_boy`` project: 103 | 104 | * They represent that they are legally entitled to grant this license, and that their 105 | contributions are their original creation 106 | 107 | * They grant the ``factory_boy`` project a perpetual, worldwide, non-exclusive, 108 | no-charge, royalty-free, irrevocable copyright license to reproduce, 109 | prepare derivative works of, publicly display, sublicense and distribute their contributions 110 | and such derivative works. 111 | 112 | * They are not expected to provide support for their contributions, except to the extent they 113 | desire to provide support. 114 | 115 | 116 | .. note:: The above agreement is inspired by the Apache Contributor License Agreement. 117 | 118 | .. vim:set ft=rst: 119 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | docs/changelog.rst -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Mark Sandstrom 2 | Copyright (c) 2011-2015 Raphaël Barrois 3 | Copyright (c) The FactoryBoy project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include ChangeLog CODE_OF_CONDUCT.md CONTRIBUTING.rst CREDITS LICENSE README.rst 2 | include Makefile tox.ini 3 | 4 | graft factory 5 | 6 | graft docs 7 | graft examples 8 | graft tests 9 | 10 | exclude .readthedocs.yaml 11 | global-exclude *.py[cod] __pycache__ .*.sw[po] 12 | prune .github 13 | prune docs/_build 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE=factory 2 | TESTS_DIR=tests 3 | DOC_DIR=docs 4 | EXAMPLES_DIR=examples 5 | SETUP_PY=setup.py 6 | 7 | # Use current python binary instead of system default. 8 | COVERAGE = python $(shell which coverage) 9 | FLAKE8 = flake8 10 | ISORT = isort 11 | CTAGS = ctags 12 | 13 | 14 | all: default 15 | 16 | 17 | default: 18 | 19 | 20 | # Package management 21 | # ================== 22 | 23 | 24 | # DOC: Remove temporary or compiled files 25 | clean: 26 | find . -type f -name '*.pyc' -delete 27 | find . -type f -path '*/__pycache__/*' -delete 28 | find . -type d -empty -delete 29 | @rm -rf tmp_test/ 30 | 31 | 32 | # DOC: Install and/or upgrade dependencies 33 | update: 34 | pip install --upgrade pip setuptools 35 | pip install --upgrade --editable .[dev,doc] 36 | pip freeze 37 | 38 | 39 | release: 40 | fullrelease 41 | 42 | 43 | .PHONY: clean update release 44 | 45 | 46 | # Tests and quality 47 | # ================= 48 | 49 | 50 | # DOC: Run tests for all supported versions (creates a set of virtualenvs) 51 | testall: 52 | tox 53 | 54 | # DOC: Run tests for the currently installed version 55 | # Remove cgi warning when dropping support for Django 3.2. 56 | test: 57 | mypy --ignore-missing-imports tests/test_typing.py 58 | python \ 59 | -b \ 60 | -X dev \ 61 | -Werror \ 62 | -Wignore:::mongomock: \ 63 | -Wignore:::mongomock.__version__: \ 64 | -Wignore:::pkg_resources: \ 65 | -m unittest 66 | 67 | # DOC: Test the examples 68 | example-test: 69 | $(MAKE) -C $(EXAMPLES_DIR) test 70 | 71 | 72 | 73 | # Note: we run the linter in two runs, because our __init__.py files has specific warnings we want to exclude 74 | # DOC: Perform code quality tasks 75 | lint: 76 | $(FLAKE8) --exclude $(PACKAGE)/__init__.py $(EXAMPLES_DIR) $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) 77 | $(FLAKE8) --ignore F401 $(PACKAGE)/__init__.py 78 | $(ISORT) --check-only --diff $(EXAMPLES_DIR) $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) 79 | check-manifest 80 | 81 | coverage: 82 | $(COVERAGE) erase 83 | $(COVERAGE) run --branch -m unittest 84 | $(COVERAGE) report 85 | $(COVERAGE) html 86 | 87 | 88 | .PHONY: test testall example-test lint coverage 89 | 90 | 91 | # Development 92 | # =========== 93 | 94 | # DOC: Generate a "tags" file 95 | TAGS: 96 | $(CTAGS) --recurse $(PACKAGE) $(TESTS_DIR) 97 | 98 | .PHONY: TAGS 99 | 100 | 101 | # Documentation 102 | # ============= 103 | 104 | 105 | # DOC: Compile the documentation 106 | doc: 107 | $(MAKE) -C $(DOC_DIR) SPHINXOPTS="-n -W" html 108 | 109 | linkcheck: 110 | $(MAKE) -C $(DOC_DIR) linkcheck 111 | 112 | spelling: 113 | $(MAKE) -C $(DOC_DIR) SPHINXOPTS=-W spelling 114 | 115 | # DOC: Show this help message 116 | help: 117 | @grep -A1 '^# DOC:' Makefile \ 118 | | awk ' \ 119 | BEGIN { FS="\n"; RS="--\n"; opt_len=0; } \ 120 | { \ 121 | doc=$$1; name=$$2; \ 122 | sub("# DOC: ", "", doc); \ 123 | sub(":", "", name); \ 124 | if (length(name) > opt_len) { \ 125 | opt_len = length(name) \ 126 | } \ 127 | opts[NR] = name; \ 128 | docs[name] = doc; \ 129 | } \ 130 | END { \ 131 | pat="%-" (opt_len + 4) "s %s\n"; \ 132 | asort(opts); \ 133 | for (i in opts) { \ 134 | opt=opts[i]; \ 135 | printf pat, opt, docs[opt] \ 136 | } \ 137 | }' 138 | 139 | 140 | .PHONY: doc linkcheck help 141 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/.keep_dir: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FactoryBoy/factory_boy/68feb45e182f9acccfde671b7ba0babb5bc7ce11/docs/_static/.keep_dir -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | import os 10 | import sys 11 | 12 | # If extensions (or modules to document with autodoc) are in another directory, 13 | # add these directories to sys.path here. If the directory is relative to the 14 | # documentation root, use os.path.abspath to make it absolute, like shown here. 15 | sys.path.insert(0, os.path.dirname(os.path.abspath('.'))) 16 | 17 | # Must be imported after the parent directory was added to sys.path for global sphinx installation. 18 | import factory # noqa 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'Factory Boy' 23 | copyright = '2011-2015, Raphaël Barrois, Mark Sandstrom' 24 | author = 'Raphaël Barrois, Mark Sandstrom' 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = factory.__version__ 28 | # The short X.Y version. 29 | version = '.'.join(release.split('.')[:2]) 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | 'sphinx.ext.autodoc', 38 | 'sphinx.ext.extlinks', 39 | 'sphinx.ext.intersphinx', 40 | 'sphinx.ext.viewcode', 41 | ] 42 | 43 | extlinks = { 44 | 'issue': ('https://github.com/FactoryBoy/factory_boy/issues/%s', 'issue %s'), 45 | 'pr': ('https://github.com/FactoryBoy/factory_boy/pull/%s', 'pull request %s'), 46 | } 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # List of patterns, relative to source directory, that match files and 55 | # directories to ignore when looking for source files. 56 | exclude_patterns = ['_build'] 57 | 58 | 59 | # -- Options for HTML output ------------------------------------------------- 60 | 61 | # The theme to use for HTML and HTML Help pages. See the documentation for 62 | # a list of builtin themes. 63 | html_theme = 'sphinx_rtd_theme' 64 | 65 | if 'READTHEDOCS_VERSION' in os.environ: 66 | # Use the readthedocs version string in preference to our known version. 67 | html_title = "{} {} documentation".format( 68 | project, os.environ['READTHEDOCS_VERSION']) 69 | 70 | # Add any paths that contain custom static files (such as style sheets) here, 71 | # relative to this directory. They are copied after the builtin static files, 72 | # so a file named "default.css" will overwrite the builtin "default.css". 73 | html_static_path = ['_static'] 74 | 75 | 76 | # -- linkcheck --------------------------------------------------------------- 77 | linkcheck_retries = 3 78 | 79 | 80 | # -- intersphinx ------------------------------------------------------------- 81 | intersphinx_mapping = { 82 | 'python': ('https://docs.python.org/3', None), 83 | 'django': ( 84 | 'https://docs.djangoproject.com/en/dev/', 85 | 'https://docs.djangoproject.com/en/dev/_objects/', 86 | ), 87 | 'mongoengine': ( 88 | 'https://mongoengine-odm.readthedocs.io/', 89 | None, 90 | ), 91 | 'sqlalchemy': ( 92 | 'https://docs.sqlalchemy.org/en/latest/', 93 | 'https://docs.sqlalchemy.org/en/latest/objects.inv', 94 | ), 95 | } 96 | 97 | 98 | # -- spelling --------------------------------------------------------------- 99 | spelling_exclude_patterns = [ 100 | 'credits.rst', 101 | ] 102 | -------------------------------------------------------------------------------- /docs/credits.rst: -------------------------------------------------------------------------------- 1 | ../CREDITS -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | Here are some real-world examples of using FactoryBoy. 5 | 6 | 7 | Objects 8 | ------- 9 | 10 | First, let's define a couple of objects: 11 | 12 | 13 | .. code-block:: python 14 | 15 | class Account: 16 | def __init__(self, username, email, date_joined): 17 | self.username = username 18 | self.email = email 19 | self.date_joined = date_joined 20 | 21 | def __str__(self): 22 | return '%s (%s)' % (self.username, self.email) 23 | 24 | 25 | class Profile: 26 | 27 | GENDER_MALE = 'm' 28 | GENDER_FEMALE = 'f' 29 | GENDER_UNKNOWN = 'u' # If the user refused to give it 30 | 31 | def __init__(self, account, gender, firstname, lastname, planet='Earth'): 32 | self.account = account 33 | self.gender = gender 34 | self.firstname = firstname 35 | self.lastname = lastname 36 | self.planet = planet 37 | 38 | def __str__(self): 39 | return '%s %s (%s)' % ( 40 | self.firstname, 41 | self.lastname, 42 | self.account.username, 43 | ) 44 | 45 | Factories 46 | --------- 47 | 48 | And now, we'll define the related factories: 49 | 50 | 51 | .. code-block:: python 52 | 53 | import datetime 54 | import factory 55 | 56 | from . import objects 57 | 58 | 59 | class AccountFactory(factory.Factory): 60 | class Meta: 61 | model = objects.Account 62 | 63 | username = factory.Sequence(lambda n: 'john%s' % n) 64 | email = factory.LazyAttribute(lambda o: '%s@example.org' % o.username) 65 | date_joined = factory.LazyFunction(datetime.datetime.now) 66 | 67 | 68 | class ProfileFactory(factory.Factory): 69 | class Meta: 70 | model = objects.Profile 71 | 72 | account = factory.SubFactory(AccountFactory) 73 | gender = factory.Iterator([objects.Profile.GENDER_MALE, objects.Profile.GENDER_FEMALE]) 74 | firstname = 'John' 75 | lastname = 'Doe' 76 | 77 | 78 | 79 | We have now defined basic factories for our ``Account`` and ``Profile`` classes. 80 | 81 | If we commonly use a specific variant of our objects, we can refine a factory accordingly: 82 | 83 | 84 | .. code-block:: python 85 | 86 | class FemaleProfileFactory(ProfileFactory): 87 | gender = objects.Profile.GENDER_FEMALE 88 | firstname = 'Jane' 89 | account__username = factory.Sequence(lambda n: 'jane%s' % n) 90 | 91 | 92 | 93 | Using the factories 94 | ------------------- 95 | 96 | We can now use our factories, for tests: 97 | 98 | 99 | .. code-block:: python 100 | 101 | import unittest 102 | 103 | from . import business_logic 104 | from . import factories 105 | from . import objects 106 | 107 | 108 | class MyTestCase(unittest.TestCase): 109 | 110 | def test_send_mail(self): 111 | account = factories.AccountFactory() 112 | email = business_logic.prepare_email(account, subject='Foo', text='Bar') 113 | 114 | self.assertEqual(email.to, account.email) 115 | 116 | def test_get_profile_stats(self): 117 | profiles = [] 118 | 119 | profiles.extend(factories.ProfileFactory.create_batch(4)) 120 | profiles.extend(factories.FemaleProfileFactory.create_batch(2)) 121 | profiles.extend(factories.ProfileFactory.create_batch(2, planet="Tatooine")) 122 | 123 | stats = business_logic.profile_stats(profiles) 124 | self.assertEqual({'Earth': 6, 'Mars': 2}, stats.planets) 125 | self.assertLess(stats.genders[objects.Profile.GENDER_FEMALE], 2) 126 | 127 | 128 | Or for fixtures: 129 | 130 | .. code-block:: python 131 | 132 | from . import factories 133 | 134 | def make_objects(): 135 | factories.ProfileFactory.create_batch(size=50) 136 | 137 | # Let's create a few, known objects. 138 | factories.ProfileFactory( 139 | gender=objects.Profile.GENDER_MALE, 140 | firstname='Luke', 141 | lastname='Skywalker', 142 | planet='Tatooine', 143 | ) 144 | 145 | factories.ProfileFactory( 146 | gender=objects.Profile.GENDER_FEMALE, 147 | firstname='Leia', 148 | lastname='Organa', 149 | planet='Alderaan', 150 | ) 151 | -------------------------------------------------------------------------------- /docs/fuzzy.rst: -------------------------------------------------------------------------------- 1 | Fuzzy attributes 2 | ================ 3 | 4 | .. module:: factory.fuzzy 5 | 6 | .. note:: Now that FactoryBoy includes the :class:`factory.Faker` class, most of 7 | these built-in fuzzers are deprecated in favor of their 8 | `Faker `_ equivalents. Further 9 | discussion in :issue:`271`. 10 | 11 | Some tests may be interested in testing with fuzzy, random values. 12 | 13 | This is handled by the :mod:`factory.fuzzy` module, which provides a few 14 | random declarations. 15 | 16 | .. note:: Use ``import factory.fuzzy`` to load this module. 17 | 18 | 19 | FuzzyAttribute 20 | -------------- 21 | 22 | 23 | .. class:: FuzzyAttribute 24 | 25 | The :class:`FuzzyAttribute` uses an arbitrary callable as fuzzer. 26 | It is expected that successive calls of that function return various 27 | values. 28 | 29 | .. attribute:: fuzzer 30 | 31 | The callable that generates random values 32 | 33 | 34 | FuzzyText 35 | --------- 36 | 37 | 38 | .. class:: FuzzyText(length=12, chars=string.ascii_letters, prefix='') 39 | 40 | The :class:`FuzzyText` fuzzer yields random strings beginning with 41 | the given :attr:`prefix`, followed by :attr:`length` characters chosen 42 | from the :attr:`chars` character set, 43 | and ending with the given :attr:`suffix`. 44 | 45 | .. attribute:: length 46 | 47 | int, the length of the random part 48 | 49 | .. attribute:: prefix 50 | 51 | text, an optional prefix to prepend to the random part 52 | 53 | .. attribute:: suffix 54 | 55 | text, an optional suffix to append to the random part 56 | 57 | .. attribute:: chars 58 | 59 | char iterable, the chars to choose from; defaults to the list of ascii 60 | letters and numbers. 61 | 62 | 63 | FuzzyChoice 64 | ----------- 65 | 66 | 67 | .. class:: FuzzyChoice(choices) 68 | 69 | The :class:`FuzzyChoice` fuzzer yields random choices from the given 70 | iterable. 71 | 72 | .. note:: The passed in :attr:`choices` will be converted into a list upon 73 | first use, not at declaration time. 74 | 75 | This allows passing in, for instance, a Django queryset that will 76 | only hit the database during the database, not at import time. 77 | 78 | .. attribute:: choices 79 | 80 | The list of choices to select randomly 81 | 82 | 83 | FuzzyInteger 84 | ------------ 85 | 86 | .. class:: FuzzyInteger(low[, high[, step]]) 87 | 88 | The :class:`FuzzyInteger` fuzzer generates random integers within a given 89 | inclusive range. 90 | 91 | The :attr:`low` bound may be omitted, in which case it defaults to 0: 92 | 93 | .. code-block:: pycon 94 | 95 | >>> fi = FuzzyInteger(0, 42) 96 | >>> fi.low, fi.high 97 | 0, 42 98 | 99 | >>> fi = FuzzyInteger(42) 100 | >>> fi.low, fi.high 101 | 0, 42 102 | 103 | .. attribute:: low 104 | 105 | int, the inclusive lower bound of generated integers 106 | 107 | .. attribute:: high 108 | 109 | int, the inclusive higher bound of generated integers 110 | 111 | .. attribute:: step 112 | 113 | int, the step between values in the range; for instance, a ``FuzzyInteger(0, 42, step=3)`` 114 | might only yield values from ``[0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42]``. 115 | 116 | 117 | FuzzyDecimal 118 | ------------ 119 | 120 | .. class:: FuzzyDecimal(low[, high[, precision=2]]) 121 | 122 | The :class:`FuzzyDecimal` fuzzer generates random :class:`decimals ` within a given 123 | inclusive range. 124 | 125 | The :attr:`low` bound may be omitted, in which case it defaults to 0: 126 | 127 | .. code-block:: pycon 128 | 129 | >>> FuzzyDecimal(0.5, 42.7) 130 | >>> fi.low, fi.high 131 | 0.5, 42.7 132 | 133 | >>> fi = FuzzyDecimal(42.7) 134 | >>> fi.low, fi.high 135 | 0.0, 42.7 136 | 137 | >>> fi = FuzzyDecimal(0.5, 42.7, 3) 138 | >>> fi.low, fi.high, fi.precision 139 | 0.5, 42.7, 3 140 | 141 | .. attribute:: low 142 | 143 | decimal, the inclusive lower bound of generated decimals 144 | 145 | .. attribute:: high 146 | 147 | decimal, the inclusive higher bound of generated decimals 148 | 149 | .. attribute:: precision 150 | int, the number of digits to generate after the dot. The default is 2 digits. 151 | 152 | 153 | FuzzyFloat 154 | ---------- 155 | 156 | .. class:: FuzzyFloat(low[, high]) 157 | 158 | The :class:`FuzzyFloat` fuzzer provides random :class:`float` objects within a given inclusive range. 159 | 160 | .. code-block:: pycon 161 | 162 | >>> FuzzyFloat(0.5, 42.7) 163 | >>> fi.low, fi.high 164 | 0.5, 42.7 165 | 166 | >>> fi = FuzzyFloat(42.7) 167 | >>> fi.low, fi.high 168 | 0.0, 42.7 169 | 170 | 171 | .. attribute:: low 172 | 173 | decimal, the inclusive lower bound of generated floats 174 | 175 | .. attribute:: high 176 | 177 | decimal, the inclusive higher bound of generated floats 178 | 179 | FuzzyDate 180 | --------- 181 | 182 | .. class:: FuzzyDate(start_date[, end_date]) 183 | 184 | The :class:`FuzzyDate` fuzzer generates random dates within a given 185 | inclusive range. 186 | 187 | The :attr:`end_date` bound may be omitted, in which case it defaults to the current date: 188 | 189 | .. code-block:: pycon 190 | 191 | >>> fd = FuzzyDate(datetime.date(2008, 1, 1)) 192 | >>> fd.start_date, fd.end_date 193 | datetime.date(2008, 1, 1), datetime.date(2013, 4, 16) 194 | 195 | .. attribute:: start_date 196 | 197 | :class:`datetime.date`, the inclusive lower bound of generated dates 198 | 199 | .. attribute:: end_date 200 | 201 | :class:`datetime.date`, the inclusive higher bound of generated dates 202 | 203 | 204 | FuzzyDateTime 205 | ------------- 206 | 207 | .. class:: FuzzyDateTime(start_dt[, end_dt], force_year=None, force_month=None, force_day=None, force_hour=None, force_minute=None, force_second=None, force_microsecond=None) 208 | 209 | The :class:`FuzzyDateTime` fuzzer generates random timezone-aware datetime within a given 210 | inclusive range. 211 | 212 | The :attr:`end_dt` bound may be omitted, in which case it defaults to ``datetime.datetime.now()`` 213 | localized into the UTC timezone. 214 | 215 | .. code-block:: pycon 216 | 217 | >>> fdt = FuzzyDateTime(datetime.datetime(2008, 1, 1, tzinfo=UTC)) 218 | >>> fdt.start_dt, fdt.end_dt 219 | datetime.datetime(2008, 1, 1, tzinfo=UTC), datetime.datetime(2013, 4, 21, 19, 13, 32, 458487, tzinfo=UTC) 220 | 221 | 222 | The ``force_XXX`` keyword arguments force the related value of generated datetimes: 223 | 224 | .. code-block:: pycon 225 | 226 | >>> fdt = FuzzyDateTime(datetime.datetime(2008, 1, 1, tzinfo=UTC), datetime.datetime(2009, 1, 1, tzinfo=UTC), 227 | ... force_day=3, force_second=42) 228 | >>> fdt.evaluate(2, None, False) # Actual code used by ``SomeFactory.build()`` 229 | datetime.datetime(2008, 5, 3, 12, 13, 42, 124848, tzinfo=UTC) 230 | 231 | 232 | .. attribute:: start_dt 233 | 234 | :class:`datetime.datetime`, the inclusive lower bound of generated datetimes 235 | 236 | .. attribute:: end_dt 237 | 238 | :class:`datetime.datetime`, the inclusive upper bound of generated datetimes 239 | 240 | 241 | .. attribute:: force_year 242 | 243 | int or None; if set, forces the :attr:`~datetime.datetime.year` of generated datetime. 244 | 245 | .. attribute:: force_month 246 | 247 | int or None; if set, forces the :attr:`~datetime.datetime.month` of generated datetime. 248 | 249 | .. attribute:: force_day 250 | 251 | int or None; if set, forces the :attr:`~datetime.datetime.day` of generated datetime. 252 | 253 | .. attribute:: force_hour 254 | 255 | int or None; if set, forces the :attr:`~datetime.datetime.hour` of generated datetime. 256 | 257 | .. attribute:: force_minute 258 | 259 | int or None; if set, forces the :attr:`~datetime.datetime.minute` of generated datetime. 260 | 261 | .. attribute:: force_second 262 | 263 | int or None; if set, forces the :attr:`~datetime.datetime.second` of generated datetime. 264 | 265 | .. attribute:: force_microsecond 266 | 267 | int or None; if set, forces the :attr:`~datetime.datetime.microsecond` of generated datetime. 268 | 269 | 270 | FuzzyNaiveDateTime 271 | ------------------ 272 | 273 | .. class:: FuzzyNaiveDateTime(start_dt[, end_dt], force_year=None, force_month=None, force_day=None, force_hour=None, force_minute=None, force_second=None, force_microsecond=None) 274 | 275 | The :class:`FuzzyNaiveDateTime` fuzzer generates random naive datetime within a given 276 | inclusive range. 277 | 278 | The :attr:`end_dt` bound may be omitted, in which case it defaults to ``datetime.datetime.now()``: 279 | 280 | .. code-block:: pycon 281 | 282 | >>> fdt = FuzzyNaiveDateTime(datetime.datetime(2008, 1, 1)) 283 | >>> fdt.start_dt, fdt.end_dt 284 | datetime.datetime(2008, 1, 1), datetime.datetime(2013, 4, 21, 19, 13, 32, 458487) 285 | 286 | 287 | The ``force_XXX`` keyword arguments force the related value of generated datetimes: 288 | 289 | .. code-block:: pycon 290 | 291 | >>> fdt = FuzzyNaiveDateTime(datetime.datetime(2008, 1, 1), datetime.datetime(2009, 1, 1), 292 | ... force_day=3, force_second=42) 293 | >>> fdt.evaluate(2, None, False) # Actual code used by ``SomeFactory.build()`` 294 | datetime.datetime(2008, 5, 3, 12, 13, 42, 124848) 295 | 296 | 297 | .. attribute:: start_dt 298 | 299 | :class:`datetime.datetime`, the inclusive lower bound of generated datetimes 300 | 301 | .. attribute:: end_dt 302 | 303 | :class:`datetime.datetime`, the inclusive upper bound of generated datetimes 304 | 305 | 306 | .. attribute:: force_year 307 | 308 | int or None; if set, forces the :attr:`~datetime.datetime.year` of generated datetime. 309 | 310 | .. attribute:: force_month 311 | 312 | int or None; if set, forces the :attr:`~datetime.datetime.month` of generated datetime. 313 | 314 | .. attribute:: force_day 315 | 316 | int or None; if set, forces the :attr:`~datetime.datetime.day` of generated datetime. 317 | 318 | .. attribute:: force_hour 319 | 320 | int or None; if set, forces the :attr:`~datetime.datetime.hour` of generated datetime. 321 | 322 | .. attribute:: force_minute 323 | 324 | int or None; if set, forces the :attr:`~datetime.datetime.minute` of generated datetime. 325 | 326 | .. attribute:: force_second 327 | 328 | int or None; if set, forces the :attr:`~datetime.datetime.second` of generated datetime. 329 | 330 | .. attribute:: force_microsecond 331 | 332 | int or None; if set, forces the :attr:`~datetime.datetime.microsecond` of generated datetime. 333 | 334 | 335 | Custom fuzzy fields 336 | ------------------- 337 | 338 | Alternate fuzzy fields may be defined. 339 | They should inherit from the :class:`BaseFuzzyAttribute` class, and override its 340 | :meth:`~BaseFuzzyAttribute.fuzz` method. 341 | 342 | 343 | .. class:: BaseFuzzyAttribute 344 | 345 | Base class for all fuzzy attributes. 346 | 347 | .. method:: fuzz(self) 348 | 349 | The method responsible for generating random values. 350 | *Must* be overridden in subclasses. 351 | 352 | .. warning:: 353 | 354 | Custom :class:`BaseFuzzyAttribute` subclasses **MUST** 355 | use :obj:`factory.random.randgen` as a randomness source; this ensures that 356 | data they generate can be regenerated using the simple state from 357 | :meth:`factory.random.get_random_state`. 358 | -------------------------------------------------------------------------------- /docs/ideas.rst: -------------------------------------------------------------------------------- 1 | Ideas 2 | ===== 3 | 4 | 5 | This is a list of future features that may be incorporated into factory_boy: 6 | 7 | * When a :class:`~factory.Factory` is built or created, pass the calling context throughout the calling chain instead of custom solutions everywhere 8 | * Define a proper set of rules for the support of third-party ORMs 9 | * Properly evaluate nested declarations (e.g ``factory.fuzzy.FuzzyDate(start_date=factory.SelfAttribute('since'))``) 10 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | 4 | 5 | Contents, indices and tables 6 | ---------------------------- 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | introduction 12 | reference 13 | orms 14 | recipes 15 | fuzzy 16 | examples 17 | internals 18 | changelog 19 | credits 20 | ideas 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | -------------------------------------------------------------------------------- /docs/internals.rst: -------------------------------------------------------------------------------- 1 | Internals 2 | ========= 3 | 4 | .. currentmodule:: factory 5 | 6 | Behind the scenes: steps performed when parsing a factory declaration, and when calling it. 7 | 8 | 9 | This section will be based on the following factory declaration: 10 | 11 | .. literalinclude:: ../tests/test_docs_internals.py 12 | :pyobject: UserFactory 13 | 14 | 15 | Parsing, Step 1: Metaclass and type declaration 16 | ----------------------------------------------- 17 | 18 | 1. Python parses the declaration and calls (thanks to the metaclass declaration): 19 | 20 | .. code-block:: python 21 | 22 | factory.base.BaseFactory.__new__( 23 | 'UserFactory', 24 | (factory.Factory,), 25 | attributes, 26 | ) 27 | 28 | 2. That metaclass removes :attr:`~Factory.Meta` and :attr:`~Factory.Params` from the class attributes, 29 | then generate the actual factory class (according to standard Python rules) 30 | 3. It initializes a :class:`FactoryOptions` object, and links it to the class 31 | 32 | 33 | Parsing, Step 2: adapting the class definition 34 | ----------------------------------------------- 35 | 36 | 1. The :class:`FactoryOptions` reads the options from the :attr:`class Meta ` declaration 37 | 2. It finds a few specific pointer (loading the model class, finding the reference 38 | factory for the sequence counter, etc.) 39 | 3. It copies declarations and parameters from parent classes 40 | 4. It scans current class attributes (from ``vars()``) to detect pre/post declarations 41 | 5. Declarations are split among pre-declarations and post-declarations 42 | (a raw value shadowing a post-declaration is seen as a post-declaration) 43 | 44 | 45 | .. note:: A declaration for ``foo__bar`` will be converted into parameter ``bar`` 46 | for declaration ``foo``. 47 | 48 | 49 | Instantiating, Step 1: Converging entry points 50 | ---------------------------------------------- 51 | 52 | First, decide the strategy: 53 | 54 | - If the entry point is specific to a strategy (:meth:`~Factory.build`, 55 | :meth:`~Factory.create_batch`, ...), use it 56 | - If it is generic (:meth:`~Factory.generate`, :meth:`Factory.__call__`), 57 | use the strategy defined at the :attr:`class Meta ` level 58 | 59 | 60 | Then, we'll pass the strategy and passed-in overrides to the ``Factory._generate`` method. 61 | 62 | .. note:: According to the project road map, a future version will use a ``Factory._generate_batch`` at its core instead. 63 | 64 | A factory's ``Factory._generate`` function actually delegates to a ``StepBuilder()`` object. 65 | This object will carry the overall "build an object" context (strategy, depth, and possibly other). 66 | 67 | 68 | Instantiating, Step 2: Preparing values 69 | --------------------------------------- 70 | 71 | 1. The ``StepBuilder`` merges overrides with the class-level declarations 72 | 2. The sequence counter for this instance is initialized 73 | 3. A ``Resolver`` is set up with all those declarations, and parses them in order; 74 | it will call each value's ``evaluate()`` method, including extra parameters. 75 | 4. If needed, the ``Resolver`` might recurse (through the ``StepBuilder``, e.g when 76 | encountering a :class:`SubFactory`. 77 | 78 | 79 | Instantiating, Step 3: Building the object 80 | ------------------------------------------ 81 | 82 | 1. The ``StepBuilder`` fetches the attributes computed by the ``Resolver``. 83 | 2. It applies renaming/adjustment rules 84 | 3. It passes them to the ``FactoryOptions.instantiate`` method, which 85 | forwards to the proper methods. 86 | 4. Post-declaration are applied (in declaration order) 87 | 88 | 89 | .. note:: This document discusses implementation details; there is no guarantee that the 90 | described methods names and signatures will be kept as is. 91 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | 5 | The purpose of factory_boy is to provide a default way of getting a new instance, 6 | while still being able to override some fields on a per-call basis. 7 | 8 | 9 | .. note:: This section will drive you through an overview of factory_boy's feature. 10 | New users are advised to spend a few minutes browsing through this list 11 | of useful helpers. 12 | 13 | Users looking for quick helpers may take a look at :doc:`recipes`, 14 | while those needing detailed documentation will be interested in the :doc:`reference` section. 15 | 16 | 17 | Basic usage 18 | ----------- 19 | 20 | 21 | Factories declare a set of attributes used to instantiate an object, whose class is defined in the ``class Meta``'s ``model`` attribute: 22 | 23 | - Subclass ``factory.Factory`` (or a more suitable subclass) 24 | - Add a ``class Meta:`` block 25 | - Set its ``model`` attribute to the target class 26 | - Add defaults for keyword args to pass to the associated class' ``__init__`` method 27 | 28 | 29 | .. code-block:: python 30 | 31 | import factory 32 | from . import base 33 | 34 | class UserFactory(factory.Factory): 35 | class Meta: 36 | model = base.User 37 | 38 | firstname = "John" 39 | lastname = "Doe" 40 | 41 | You may now get ``base.User`` instances trivially: 42 | 43 | .. code-block:: pycon 44 | 45 | >>> john = UserFactory() 46 | 47 | 48 | It is also possible to override the defined attributes by passing keyword arguments to the factory: 49 | 50 | .. code-block:: pycon 51 | 52 | >>> jack = UserFactory(firstname="Jack") 53 | 54 | 55 | 56 | A given class may be associated to many :class:`~factory.Factory` subclasses: 57 | 58 | .. code-block:: python 59 | 60 | class EnglishUserFactory(factory.Factory): 61 | class Meta: 62 | model = base.User 63 | 64 | firstname = "John" 65 | lastname = "Doe" 66 | lang = 'en' 67 | 68 | 69 | class FrenchUserFactory(factory.Factory): 70 | class Meta: 71 | model = base.User 72 | 73 | firstname = "Jean" 74 | lastname = "Dupont" 75 | lang = 'fr' 76 | 77 | 78 | .. code-block:: pycon 79 | 80 | >>> EnglishUserFactory() 81 | 82 | >>> FrenchUserFactory() 83 | 84 | 85 | 86 | Sequences 87 | --------- 88 | 89 | When a field has a unique key, each object generated by the factory should have a different value for that field. 90 | This is achieved with the :class:`~factory.Sequence` declaration: 91 | 92 | .. code-block:: python 93 | 94 | class UserFactory(factory.Factory): 95 | class Meta: 96 | model = models.User 97 | 98 | username = factory.Sequence(lambda n: 'user%d' % n) 99 | 100 | .. code-block:: pycon 101 | 102 | >>> # The sequence counter starts at 0 by default 103 | >>> UserFactory() 104 | 105 | >>> UserFactory() 106 | 107 | 108 | >>> # A value can be provided for a sequence-driven field 109 | >>> # but this still increments the sequence counter 110 | >>> UserFactory(username="ada.lovelace") 111 | 112 | >>> UserFactory() 113 | 114 | 115 | .. note:: For more complex situations, you may also use the :meth:`@factory.sequence ` decorator (note that ``self`` is not added as first parameter): 116 | 117 | .. code-block:: python 118 | 119 | class UserFactory(factory.Factory): 120 | class Meta: 121 | model = models.User 122 | 123 | @factory.sequence 124 | def username(n): 125 | return 'user%d' % n 126 | 127 | To set or reset the sequence counter see :ref:`Forcing a sequence counter `. 128 | 129 | LazyFunction 130 | ------------ 131 | 132 | In simple cases, calling a function is enough to compute the value. If that function doesn't depend on the object 133 | being built, use :class:`~factory.LazyFunction` to call that function; it should receive a function taking no 134 | argument and returning the value for the field: 135 | 136 | .. code-block:: python 137 | 138 | class LogFactory(factory.Factory): 139 | class Meta: 140 | model = models.Log 141 | 142 | timestamp = factory.LazyFunction(datetime.now) 143 | 144 | .. code-block:: pycon 145 | 146 | >>> LogFactory() 147 | 148 | 149 | >>> # The LazyFunction can be overridden 150 | >>> LogFactory(timestamp=now - timedelta(days=1)) 151 | 152 | 153 | 154 | .. note:: For complex cases when you happen to write a specific function, 155 | the :meth:`@factory.lazy_attribute ` decorator should be more appropriate. 156 | 157 | 158 | LazyAttribute 159 | ------------- 160 | 161 | Some fields may be deduced from others, for instance the email based on the username. 162 | The :class:`~factory.LazyAttribute` handles such cases: it should receive a function 163 | taking the object being built and returning the value for the field: 164 | 165 | .. code-block:: python 166 | 167 | class UserFactory(factory.Factory): 168 | class Meta: 169 | model = models.User 170 | 171 | username = factory.Sequence(lambda n: 'user%d' % n) 172 | email = factory.LazyAttribute(lambda obj: '%s@example.com' % obj.username) 173 | 174 | .. code-block:: pycon 175 | 176 | >>> UserFactory() 177 | 178 | 179 | >>> # The LazyAttribute handles overridden fields 180 | >>> UserFactory(username='john') 181 | 182 | 183 | >>> # They can be directly overridden as well 184 | >>> UserFactory(email='doe@example.com') 185 | 186 | 187 | 188 | .. note:: As for :class:`~factory.Sequence`, a :meth:`@factory.lazy_attribute ` decorator is available: 189 | 190 | 191 | .. code-block:: python 192 | 193 | class UserFactory(factory.Factory): 194 | class Meta: 195 | model = models.User 196 | 197 | username = factory.Sequence(lambda n: 'user%d' % n) 198 | 199 | @factory.lazy_attribute 200 | def email(self): 201 | return '%s@example.com' % self.username 202 | 203 | 204 | Inheritance 205 | ----------- 206 | 207 | 208 | Once a "base" factory has been defined for a given class, 209 | alternate versions can be easily defined through subclassing. 210 | 211 | The subclassed :class:`~factory.Factory` will inherit all declarations from its parent, 212 | and update them with its own declarations: 213 | 214 | .. code-block:: python 215 | 216 | class UserFactory(factory.Factory): 217 | class Meta: 218 | model = base.User 219 | 220 | firstname = "John" 221 | lastname = "Doe" 222 | group = 'users' 223 | 224 | class AdminFactory(UserFactory): 225 | admin = True 226 | group = 'admins' 227 | 228 | .. code-block:: pycon 229 | 230 | >>> user = UserFactory() 231 | >>> user 232 | 233 | >>> user.group 234 | 'users' 235 | 236 | >>> admin = AdminFactory() 237 | >>> admin 238 | 239 | >>> admin.group # The AdminFactory field has overridden the base field 240 | 'admins' 241 | 242 | 243 | Any argument of all factories in the chain can easily be overridden: 244 | 245 | .. code-block:: pycon 246 | 247 | >>> super_admin = AdminFactory(group='superadmins', lastname="Lennon") 248 | >>> super_admin 249 | 250 | >>> super_admin.group # Overridden at call time 251 | 'superadmins' 252 | 253 | 254 | Non-kwarg arguments 255 | ------------------- 256 | 257 | Some classes take a few, non-kwarg arguments first. 258 | 259 | This is handled by the :data:`~factory.FactoryOptions.inline_args` attribute: 260 | 261 | .. code-block:: python 262 | 263 | class MyFactory(factory.Factory): 264 | class Meta: 265 | model = MyClass 266 | inline_args = ('x', 'y') 267 | 268 | x = 1 269 | y = 2 270 | z = 3 271 | 272 | .. code-block:: pycon 273 | 274 | >>> MyFactory(y=4) 275 | 276 | 277 | 278 | Altering a factory's behavior: parameters and traits 279 | ---------------------------------------------------- 280 | 281 | Some classes are better described with a few, simple parameters, that aren't fields on the actual model. 282 | In that case, use a :attr:`~factory.Factory.Params` declaration: 283 | 284 | .. code-block:: python 285 | 286 | class RentalFactory(factory.Factory): 287 | class Meta: 288 | model = Rental 289 | 290 | begin = factory.fuzzy.FuzzyDate(start_date=datetime.date(2000, 1, 1)) 291 | end = factory.LazyAttribute(lambda o: o.begin + o.duration) 292 | 293 | class Params: 294 | duration = 12 295 | 296 | .. code-block:: pycon 297 | 298 | >>> RentalFactory(duration=0) 299 | 2012-03-03> 300 | >>> RentalFactory(duration=10) 301 | 2012-12-26> 302 | 303 | 304 | When many fields should be updated based on a flag, use :class:`Traits ` instead: 305 | 306 | .. code-block:: python 307 | 308 | class OrderFactory(factory.Factory): 309 | status = 'pending' 310 | shipped_by = None 311 | shipped_on = None 312 | 313 | class Meta: 314 | model = Order 315 | 316 | class Params: 317 | shipped = factory.Trait( 318 | status='shipped', 319 | shipped_by=factory.SubFactory(EmployeeFactory), 320 | shipped_on=factory.LazyFunction(datetime.date.today), 321 | ) 322 | 323 | A trait is toggled by a single boolean value: 324 | 325 | .. code-block:: pycon 326 | 327 | >>> OrderFactory() 328 | 329 | >>> OrderFactory(shipped=True) 330 | 331 | 332 | 333 | Strategies 334 | ---------- 335 | 336 | All factories support two built-in strategies: 337 | 338 | * ``build`` provides a local object 339 | * ``create`` instantiates a local object, and saves it to the database. 340 | 341 | .. note:: For 1.X versions, the ``create`` will actually call ``AssociatedClass.objects.create``, 342 | as for a Django model. 343 | 344 | Starting from 2.0, :meth:`factory.Factory.create` simply calls ``AssociatedClass(**kwargs)``. 345 | You should use :class:`~factory.django.DjangoModelFactory` for Django models. 346 | 347 | 348 | When a :class:`~factory.Factory` includes related fields (:class:`~factory.SubFactory`, :class:`~factory.RelatedFactory`), 349 | the parent's strategy will be pushed onto related factories. 350 | 351 | 352 | Calling a :class:`~factory.Factory` subclass will provide an object through the default strategy: 353 | 354 | .. code-block:: python 355 | 356 | class MyFactory(factory.Factory): 357 | class Meta: 358 | model = MyClass 359 | 360 | .. code-block:: pycon 361 | 362 | >>> MyFactory.create() 363 | 364 | 365 | >>> MyFactory.build() 366 | 367 | 368 | >>> MyFactory() # equivalent to MyFactory.create() 369 | 370 | 371 | 372 | The default strategy can be changed by setting the ``class Meta`` :attr:`~factory.FactoryOptions.strategy` attribute. 373 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FactoryBoy/factory_boy/68feb45e182f9acccfde671b7ba0babb5bc7ce11/docs/logo.png -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 27 | 31 | 35 | 36 | 38 | 42 | 46 | 47 | 57 | 59 | 63 | 67 | 68 | 78 | 80 | 84 | 88 | 89 | 99 | 100 | 128 | 131 | 132 | 134 | 135 | 137 | image/svg+xml 138 | 140 | 141 | 142 | 143 | 144 | 148 | 154 | 155 | 161 | 166 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | args 2 | backends 3 | backtrace 4 | boolean 5 | datastore 6 | datetimes 7 | dicts 8 | Django 9 | filename 10 | fuzzer 11 | fuzzers 12 | fuzzying 13 | getter 14 | instantiation 15 | iterable 16 | iterables 17 | kwarg 18 | kwargs 19 | metaclass 20 | misconfiguration 21 | Mogo 22 | MongoDB 23 | mongoengine 24 | pre 25 | prepend 26 | pymongo 27 | queryset 28 | recurse 29 | subclassed 30 | subclasses 31 | subclassing 32 | subfactories 33 | thoughtbot 34 | tox 35 | unexplicit 36 | username 37 | lookup 38 | -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | EXAMPLES = django_demo flask_alchemy 2 | 3 | TEST_TARGETS = $(addprefix runtest-,$(EXAMPLES)) 4 | 5 | test: $(TEST_TARGETS) 6 | 7 | 8 | $(TEST_TARGETS): runtest-%: 9 | cd $* && ./runtests.sh 10 | -------------------------------------------------------------------------------- /examples/django_demo/django_demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FactoryBoy/factory_boy/68feb45e182f9acccfde671b7ba0babb5bc7ce11/examples/django_demo/django_demo/__init__.py -------------------------------------------------------------------------------- /examples/django_demo/django_demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'kh)1s3@93ju6f6$qx!758f6h^(_3d0brqzoxubo@xsn3*%2wgu' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'generic_foreignkey' 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'django_demo.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'django_demo.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | -------------------------------------------------------------------------------- /examples/django_demo/django_demo/urls.py: -------------------------------------------------------------------------------- 1 | """django_demo URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.10/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /examples/django_demo/django_demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_demo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_demo.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/django_demo/generic_foreignkey/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FactoryBoy/factory_boy/68feb45e182f9acccfde671b7ba0babb5bc7ce11/examples/django_demo/generic_foreignkey/__init__.py -------------------------------------------------------------------------------- /examples/django_demo/generic_foreignkey/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class GenericForeignKeyConfig(AppConfig): 5 | name = 'generic_foreignkey' 6 | -------------------------------------------------------------------------------- /examples/django_demo/generic_foreignkey/factories.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import Group, User 2 | from django.contrib.contenttypes.models import ContentType 3 | 4 | import factory.django 5 | 6 | from .models import TaggedItem 7 | 8 | 9 | class UserFactory(factory.django.DjangoModelFactory): 10 | first_name = 'Adam' 11 | 12 | class Meta: 13 | model = User 14 | 15 | 16 | class GroupFactory(factory.django.DjangoModelFactory): 17 | name = 'group' 18 | 19 | class Meta: 20 | model = Group 21 | 22 | 23 | class TaggedItemFactory(factory.django.DjangoModelFactory): 24 | object_id = factory.SelfAttribute('content_object.id') 25 | content_type = factory.LazyAttribute( 26 | lambda o: ContentType.objects.get_for_model(o.content_object)) 27 | 28 | class Meta: 29 | exclude = ['content_object'] 30 | abstract = True 31 | 32 | 33 | class TaggedUserFactory(TaggedItemFactory): 34 | content_object = factory.SubFactory(UserFactory) 35 | 36 | class Meta: 37 | model = TaggedItem 38 | 39 | 40 | class TaggedGroupFactory(TaggedItemFactory): 41 | content_object = factory.SubFactory(GroupFactory) 42 | 43 | class Meta: 44 | model = TaggedItem 45 | -------------------------------------------------------------------------------- /examples/django_demo/generic_foreignkey/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.db.models.deletion 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | initial = True 8 | 9 | dependencies = [ 10 | ('contenttypes', '0002_remove_content_type_name'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='TaggedItem', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('tag', models.SlugField()), 19 | ('object_id', models.PositiveIntegerField()), 20 | ( 21 | 'content_type', 22 | models.ForeignKey( 23 | on_delete=django.db.models.deletion.CASCADE, 24 | to='contenttypes.ContentType' 25 | ) 26 | ), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /examples/django_demo/generic_foreignkey/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FactoryBoy/factory_boy/68feb45e182f9acccfde671b7ba0babb5bc7ce11/examples/django_demo/generic_foreignkey/migrations/__init__.py -------------------------------------------------------------------------------- /examples/django_demo/generic_foreignkey/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.fields import GenericForeignKey 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.db import models 4 | 5 | 6 | class TaggedItem(models.Model): 7 | """Example GenericForeignKey model from django docs""" 8 | tag = models.SlugField() 9 | content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) 10 | object_id = models.PositiveIntegerField() 11 | content_object = GenericForeignKey('content_type', 'object_id') 12 | 13 | def __str__(self): 14 | return self.tag 15 | -------------------------------------------------------------------------------- /examples/django_demo/generic_foreignkey/tests.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import Group, User 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.test import TestCase 4 | 5 | from .factories import GroupFactory, TaggedGroupFactory, TaggedUserFactory, UserFactory 6 | 7 | 8 | class GenericFactoryTest(TestCase): 9 | 10 | def test_user_factory(self): 11 | user = UserFactory() 12 | self.assertEqual(user.first_name, 'Adam') 13 | 14 | def test_group_factory(self): 15 | group = GroupFactory() 16 | self.assertEqual(group.name, 'group') 17 | 18 | def test_generic_user(self): 19 | model = TaggedUserFactory(tag='user') 20 | self.assertEqual(model.tag, 'user') 21 | self.assertTrue(isinstance(model.content_object, User)) 22 | self.assertEqual(model.content_type, ContentType.objects.get_for_model(model.content_object)) 23 | 24 | def test_generic_group(self): 25 | model = TaggedGroupFactory(tag='group') 26 | self.assertEqual(model.tag, 'group') 27 | self.assertTrue(isinstance(model.content_object, Group)) 28 | self.assertEqual(model.content_type, ContentType.objects.get_for_model(model.content_object)) 29 | -------------------------------------------------------------------------------- /examples/django_demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_demo.settings") 7 | from django.core.management import execute_from_command_line 8 | execute_from_command_line(sys.argv) 9 | -------------------------------------------------------------------------------- /examples/django_demo/requirements.txt: -------------------------------------------------------------------------------- 1 | Django 2 | -------------------------------------------------------------------------------- /examples/django_demo/runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd $(dirname $0) 4 | python manage.py test; 5 | -------------------------------------------------------------------------------- /examples/flask_alchemy/demoapp.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | from flask import Flask 4 | from flask_sqlalchemy import SQLAlchemy 5 | 6 | app = Flask(__name__) 7 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' 8 | db = SQLAlchemy(app) 9 | 10 | 11 | class User(db.Model): 12 | id = db.Column(db.Integer, primary_key=True) 13 | username = db.Column(db.String(80), unique=True) 14 | email = db.Column(db.String(120), unique=True) 15 | 16 | def __init__(self, username, email): 17 | self.username = username 18 | self.email = email 19 | 20 | def __repr__(self): 21 | return '' % self.username 22 | 23 | 24 | class UserLog(db.Model): 25 | id = db.Column(db.Integer, primary_key=True) 26 | message = db.Column(db.String(1000)) 27 | 28 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 29 | user = db.relationship('User', backref=db.backref('logs', lazy='dynamic')) 30 | 31 | def __init__(self, message, user): 32 | self.message = message 33 | self.user = user 34 | 35 | def __repr__(self): 36 | return f'' 37 | -------------------------------------------------------------------------------- /examples/flask_alchemy/demoapp_factories.py: -------------------------------------------------------------------------------- 1 | import demoapp 2 | 3 | import factory.alchemy 4 | import factory.fuzzy 5 | 6 | 7 | class BaseFactory(factory.alchemy.SQLAlchemyModelFactory): 8 | class Meta: 9 | abstract = True 10 | sqlalchemy_session = demoapp.db.session 11 | 12 | 13 | class UserFactory(BaseFactory): 14 | class Meta: 15 | model = demoapp.User 16 | 17 | username = factory.fuzzy.FuzzyText() 18 | email = factory.fuzzy.FuzzyText() 19 | 20 | 21 | class UserLogFactory(BaseFactory): 22 | class Meta: 23 | model = demoapp.UserLog 24 | 25 | message = factory.fuzzy.FuzzyText() 26 | user = factory.SubFactory(UserFactory) 27 | -------------------------------------------------------------------------------- /examples/flask_alchemy/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Flask-SQLAlchemy 3 | -------------------------------------------------------------------------------- /examples/flask_alchemy/runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd $(dirname $0) 4 | for f in test_*.py; do 5 | python -m unittest discover 6 | done 7 | -------------------------------------------------------------------------------- /examples/flask_alchemy/test_demoapp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import demoapp 4 | import demoapp_factories 5 | 6 | 7 | class DemoAppTestCase(unittest.TestCase): 8 | 9 | def setUp(self): 10 | demoapp.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' 11 | demoapp.app.config['TESTING'] = True 12 | 13 | self.app_context = demoapp.app.app_context() 14 | self.app_context.push() 15 | 16 | self.app = demoapp.app.test_client() 17 | self.db = demoapp.db 18 | self.db.create_all() 19 | 20 | def tearDown(self): 21 | self.db.drop_all() 22 | self.app_context.pop() 23 | 24 | def test_user_factory(self): 25 | user = demoapp_factories.UserFactory() 26 | self.db.session.commit() 27 | self.assertIsNotNone(user.id) 28 | self.assertEqual(1, len(demoapp.User.query.all())) 29 | 30 | def test_userlog_factory(self): 31 | userlog = demoapp_factories.UserLogFactory() 32 | self.db.session.commit() 33 | self.assertIsNotNone(userlog.id) 34 | self.assertIsNotNone(userlog.user.id) 35 | self.assertEqual(1, len(demoapp.User.query.all())) 36 | self.assertEqual(1, len(demoapp.UserLog.query.all())) 37 | -------------------------------------------------------------------------------- /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | -r flask_alchemy/requirements.txt 2 | -r django_demo/requirements.txt 3 | -------------------------------------------------------------------------------- /factory/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | import importlib.metadata 4 | 5 | from .base import ( 6 | BaseDictFactory, 7 | BaseListFactory, 8 | DictFactory, 9 | Factory, 10 | ListFactory, 11 | StubFactory, 12 | use_strategy, 13 | ) 14 | from .declarations import ( 15 | ContainerAttribute, 16 | Dict, 17 | Iterator, 18 | LazyAttribute, 19 | LazyAttributeSequence, 20 | LazyFunction, 21 | List, 22 | Maybe, 23 | PostGeneration, 24 | PostGenerationMethodCall, 25 | RelatedFactory, 26 | RelatedFactoryList, 27 | SelfAttribute, 28 | Sequence, 29 | SubFactory, 30 | Trait, 31 | Transformer, 32 | ) 33 | from .enums import BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY 34 | from .errors import FactoryError 35 | from .faker import Faker 36 | from .helpers import ( 37 | build, 38 | build_batch, 39 | container_attribute, 40 | create, 41 | create_batch, 42 | debug, 43 | generate, 44 | generate_batch, 45 | iterator, 46 | lazy_attribute, 47 | lazy_attribute_sequence, 48 | make_factory, 49 | post_generation, 50 | sequence, 51 | simple_generate, 52 | simple_generate_batch, 53 | stub, 54 | stub_batch, 55 | ) 56 | 57 | try: 58 | from . import alchemy 59 | except ImportError: 60 | pass 61 | try: 62 | from . import django 63 | except ImportError: 64 | pass 65 | try: 66 | from . import mogo 67 | except ImportError: 68 | pass 69 | try: 70 | from . import mongoengine 71 | except ImportError: 72 | pass 73 | 74 | __author__ = 'Raphaël Barrois ' 75 | __version__ = importlib.metadata.version("factory_boy") 76 | -------------------------------------------------------------------------------- /factory/alchemy.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | from sqlalchemy.exc import IntegrityError 4 | from sqlalchemy.orm.exc import NoResultFound 5 | 6 | from . import base, errors 7 | 8 | SESSION_PERSISTENCE_COMMIT = 'commit' 9 | SESSION_PERSISTENCE_FLUSH = 'flush' 10 | VALID_SESSION_PERSISTENCE_TYPES = [ 11 | None, 12 | SESSION_PERSISTENCE_COMMIT, 13 | SESSION_PERSISTENCE_FLUSH, 14 | ] 15 | 16 | 17 | class SQLAlchemyOptions(base.FactoryOptions): 18 | def _check_sqlalchemy_session_persistence(self, meta, value): 19 | if value not in VALID_SESSION_PERSISTENCE_TYPES: 20 | raise TypeError( 21 | "%s.sqlalchemy_session_persistence must be one of %s, got %r" % 22 | (meta, VALID_SESSION_PERSISTENCE_TYPES, value) 23 | ) 24 | 25 | @staticmethod 26 | def _check_has_sqlalchemy_session_set(meta, value): 27 | if value is not None and getattr(meta, "sqlalchemy_session", None) is not None: 28 | raise RuntimeError("Provide either a sqlalchemy_session or a sqlalchemy_session_factory, not both") 29 | 30 | def _build_default_options(self): 31 | return super()._build_default_options() + [ 32 | base.OptionDefault('sqlalchemy_get_or_create', (), inherit=True), 33 | base.OptionDefault('sqlalchemy_session', None, inherit=True), 34 | base.OptionDefault( 35 | 'sqlalchemy_session_factory', None, inherit=True, checker=self._check_has_sqlalchemy_session_set 36 | ), 37 | base.OptionDefault( 38 | 'sqlalchemy_session_persistence', 39 | None, 40 | inherit=True, 41 | checker=self._check_sqlalchemy_session_persistence, 42 | ), 43 | ] 44 | 45 | 46 | class SQLAlchemyModelFactory(base.Factory): 47 | """Factory for SQLAlchemy models. """ 48 | 49 | _options_class = SQLAlchemyOptions 50 | _original_params = None 51 | 52 | class Meta: 53 | abstract = True 54 | 55 | @classmethod 56 | def _generate(cls, strategy, params): 57 | # Original params are used in _get_or_create if it cannot build an 58 | # object initially due to an IntegrityError being raised 59 | cls._original_params = params 60 | return super()._generate(strategy, params) 61 | 62 | @classmethod 63 | def _get_or_create(cls, model_class, session, args, kwargs): 64 | key_fields = {} 65 | for field in cls._meta.sqlalchemy_get_or_create: 66 | if field not in kwargs: 67 | raise errors.FactoryError( 68 | "sqlalchemy_get_or_create - " 69 | "Unable to find initialization value for '%s' in factory %s" % 70 | (field, cls.__name__)) 71 | key_fields[field] = kwargs.pop(field) 72 | 73 | obj = session.query(model_class).filter_by( 74 | *args, **key_fields).one_or_none() 75 | 76 | if not obj: 77 | try: 78 | obj = cls._save(model_class, session, args, {**key_fields, **kwargs}) 79 | except IntegrityError as e: 80 | session.rollback() 81 | 82 | if cls._original_params is None: 83 | raise e 84 | 85 | get_or_create_params = { 86 | lookup: value 87 | for lookup, value in cls._original_params.items() 88 | if lookup in cls._meta.sqlalchemy_get_or_create 89 | } 90 | if get_or_create_params: 91 | try: 92 | obj = session.query(model_class).filter_by( 93 | **get_or_create_params).one() 94 | except NoResultFound: 95 | # Original params are not a valid lookup and triggered a create(), 96 | # that resulted in an IntegrityError. 97 | raise e 98 | else: 99 | raise e 100 | 101 | return obj 102 | 103 | @classmethod 104 | def _create(cls, model_class, *args, **kwargs): 105 | """Create an instance of the model, and save it to the database.""" 106 | session_factory = cls._meta.sqlalchemy_session_factory 107 | if session_factory: 108 | cls._meta.sqlalchemy_session = session_factory() 109 | 110 | session = cls._meta.sqlalchemy_session 111 | 112 | if session is None: 113 | raise RuntimeError("No session provided.") 114 | if cls._meta.sqlalchemy_get_or_create: 115 | return cls._get_or_create(model_class, session, args, kwargs) 116 | return cls._save(model_class, session, args, kwargs) 117 | 118 | @classmethod 119 | def _save(cls, model_class, session, args, kwargs): 120 | session_persistence = cls._meta.sqlalchemy_session_persistence 121 | 122 | obj = model_class(*args, **kwargs) 123 | session.add(obj) 124 | if session_persistence == SESSION_PERSISTENCE_FLUSH: 125 | session.flush() 126 | elif session_persistence == SESSION_PERSISTENCE_COMMIT: 127 | session.commit() 128 | return obj 129 | -------------------------------------------------------------------------------- /factory/builder.py: -------------------------------------------------------------------------------- 1 | """Build factory instances.""" 2 | 3 | import collections 4 | 5 | from . import enums, errors, utils 6 | 7 | DeclarationWithContext = collections.namedtuple( 8 | 'DeclarationWithContext', 9 | ['name', 'declaration', 'context'], 10 | ) 11 | 12 | 13 | class DeclarationSet: 14 | """A set of declarations, including the recursive parameters. 15 | 16 | Attributes: 17 | declarations (dict(name => declaration)): the top-level declarations 18 | contexts (dict(name => dict(subfield => value))): the nested parameters related 19 | to a given top-level declaration 20 | 21 | This object behaves similarly to a dict mapping a top-level declaration name to a 22 | DeclarationWithContext, containing field name, declaration object and extra context. 23 | """ 24 | 25 | def __init__(self, initial=None): 26 | self.declarations = {} 27 | self.contexts = collections.defaultdict(dict) 28 | self.update(initial or {}) 29 | 30 | @classmethod 31 | def split(cls, entry): 32 | """Split a declaration name into a (declaration, subpath) tuple. 33 | 34 | Examples: 35 | >>> DeclarationSet.split('foo__bar') 36 | ('foo', 'bar') 37 | >>> DeclarationSet.split('foo') 38 | ('foo', None) 39 | >>> DeclarationSet.split('foo__bar__baz') 40 | ('foo', 'bar__baz') 41 | """ 42 | if enums.SPLITTER in entry: 43 | return entry.split(enums.SPLITTER, 1) 44 | else: 45 | return (entry, None) 46 | 47 | @classmethod 48 | def join(cls, root, subkey): 49 | """Rebuild a full declaration name from its components. 50 | 51 | for every string x, we have `join(split(x)) == x`. 52 | """ 53 | if subkey is None: 54 | return root 55 | return enums.SPLITTER.join((root, subkey)) 56 | 57 | def copy(self): 58 | return self.__class__(self.as_dict()) 59 | 60 | def update(self, values): 61 | """Add new declarations to this set/ 62 | 63 | Args: 64 | values (dict(name, declaration)): the declarations to ingest. 65 | """ 66 | for k, v in values.items(): 67 | root, sub = self.split(k) 68 | if sub is None: 69 | self.declarations[root] = v 70 | else: 71 | self.contexts[root][sub] = v 72 | 73 | extra_context_keys = set(self.contexts) - set(self.declarations) 74 | if extra_context_keys: 75 | raise errors.InvalidDeclarationError( 76 | "Received deep context for unknown fields: %r (known=%r)" % ( 77 | { 78 | self.join(root, sub): v 79 | for root in extra_context_keys 80 | for sub, v in self.contexts[root].items() 81 | }, 82 | sorted(self.declarations), 83 | ) 84 | ) 85 | 86 | def filter(self, entries): 87 | """Filter a set of declarations: keep only those related to this object. 88 | 89 | This will keep: 90 | - Declarations that 'override' the current ones 91 | - Declarations that are parameters to current ones 92 | """ 93 | return [ 94 | entry for entry in entries 95 | if self.split(entry)[0] in self.declarations 96 | ] 97 | 98 | def sorted(self): 99 | return utils.sort_ordered_objects( 100 | self.declarations, 101 | getter=lambda entry: self.declarations[entry], 102 | ) 103 | 104 | def __contains__(self, key): 105 | return key in self.declarations 106 | 107 | def __getitem__(self, key): 108 | return DeclarationWithContext( 109 | name=key, 110 | declaration=self.declarations[key], 111 | context=self.contexts[key], 112 | ) 113 | 114 | def __iter__(self): 115 | return iter(self.declarations) 116 | 117 | def values(self): 118 | """Retrieve the list of declarations, with their context.""" 119 | for name in self: 120 | yield self[name] 121 | 122 | def _items(self): 123 | """Extract a list of (key, value) pairs, suitable for our __init__.""" 124 | for name in self.declarations: 125 | yield name, self.declarations[name] 126 | for subkey, value in self.contexts[name].items(): 127 | yield self.join(name, subkey), value 128 | 129 | def as_dict(self): 130 | """Return a dict() suitable for our __init__.""" 131 | return dict(self._items()) 132 | 133 | def __repr__(self): 134 | return '' % self.as_dict() 135 | 136 | 137 | def _captures_overrides(declaration_with_context): 138 | declaration = declaration_with_context.declaration 139 | if enums.get_builder_phase(declaration) == enums.BuilderPhase.ATTRIBUTE_RESOLUTION: 140 | return declaration.CAPTURE_OVERRIDES 141 | else: 142 | return False 143 | 144 | 145 | def parse_declarations(decls, base_pre=None, base_post=None): 146 | pre_declarations = base_pre.copy() if base_pre else DeclarationSet() 147 | post_declarations = base_post.copy() if base_post else DeclarationSet() 148 | 149 | # Inject extra declarations, splitting between known-to-be-post and undetermined 150 | extra_post = {} 151 | extra_maybenonpost = {} 152 | for k, v in decls.items(): 153 | if enums.get_builder_phase(v) == enums.BuilderPhase.POST_INSTANTIATION: 154 | if k in pre_declarations: 155 | # Conflict: PostGenerationDeclaration with the same 156 | # name as a BaseDeclaration 157 | raise errors.InvalidDeclarationError( 158 | "PostGenerationDeclaration %s=%r shadows declaration %r" 159 | % (k, v, pre_declarations[k]) 160 | ) 161 | extra_post[k] = v 162 | elif k in post_declarations: 163 | # Passing in a scalar value to a PostGenerationDeclaration 164 | # Set it as `key__` 165 | magic_key = post_declarations.join(k, '') 166 | extra_post[magic_key] = v 167 | else: 168 | extra_maybenonpost[k] = v 169 | 170 | # Start with adding new post-declarations 171 | post_declarations.update(extra_post) 172 | 173 | # Fill in extra post-declaration context 174 | extra_pre_declarations = {} 175 | extra_post_declarations = {} 176 | post_overrides = post_declarations.filter(extra_maybenonpost) 177 | for k, v in extra_maybenonpost.items(): 178 | if k in post_overrides: 179 | extra_post_declarations[k] = v 180 | elif k in pre_declarations and _captures_overrides(pre_declarations[k]): 181 | # Send the overriding value to the existing declaration. 182 | # By symmetry with the behaviour of PostGenerationDeclaration, 183 | # we send it as `key__` -- i.e under the '' key. 184 | magic_key = pre_declarations.join(k, '') 185 | extra_pre_declarations[magic_key] = v 186 | else: 187 | # Anything else is pre_declarations 188 | extra_pre_declarations[k] = v 189 | pre_declarations.update(extra_pre_declarations) 190 | post_declarations.update(extra_post_declarations) 191 | 192 | return pre_declarations, post_declarations 193 | 194 | 195 | class BuildStep: 196 | def __init__(self, builder, sequence, parent_step=None): 197 | self.builder = builder 198 | self.sequence = sequence 199 | self.attributes = {} 200 | self.parent_step = parent_step 201 | self.stub = None 202 | 203 | def resolve(self, declarations): 204 | self.stub = Resolver( 205 | declarations=declarations, 206 | step=self, 207 | sequence=self.sequence, 208 | ) 209 | 210 | for field_name in declarations: 211 | self.attributes[field_name] = getattr(self.stub, field_name) 212 | 213 | @property 214 | def chain(self): 215 | if self.parent_step: 216 | parent_chain = self.parent_step.chain 217 | else: 218 | parent_chain = () 219 | return (self.stub,) + parent_chain 220 | 221 | def recurse(self, factory, declarations, force_sequence=None): 222 | from . import base 223 | if not issubclass(factory, base.BaseFactory): 224 | raise errors.AssociatedClassError( 225 | "%r: Attempting to recursing into a non-factory object %r" 226 | % (self, factory)) 227 | builder = self.builder.recurse(factory._meta, declarations) 228 | return builder.build(parent_step=self, force_sequence=force_sequence) 229 | 230 | def __repr__(self): 231 | return f"" 232 | 233 | 234 | class StepBuilder: 235 | """A factory instantiation step. 236 | 237 | Attributes: 238 | - parent: the parent StepBuilder, or None for the root step 239 | - extras: the passed-in kwargs for this branch 240 | - factory: the factory class being built 241 | - strategy: the strategy to use 242 | """ 243 | def __init__(self, factory_meta, extras, strategy): 244 | self.factory_meta = factory_meta 245 | self.strategy = strategy 246 | self.extras = extras 247 | self.force_init_sequence = extras.pop('__sequence', None) 248 | 249 | def build(self, parent_step=None, force_sequence=None): 250 | """Build a factory instance.""" 251 | # TODO: Handle "batch build" natively 252 | pre, post = parse_declarations( 253 | self.extras, 254 | base_pre=self.factory_meta.pre_declarations, 255 | base_post=self.factory_meta.post_declarations, 256 | ) 257 | 258 | if force_sequence is not None: 259 | sequence = force_sequence 260 | elif self.force_init_sequence is not None: 261 | sequence = self.force_init_sequence 262 | else: 263 | sequence = self.factory_meta.next_sequence() 264 | 265 | step = BuildStep( 266 | builder=self, 267 | sequence=sequence, 268 | parent_step=parent_step, 269 | ) 270 | step.resolve(pre) 271 | 272 | args, kwargs = self.factory_meta.prepare_arguments(step.attributes) 273 | 274 | instance = self.factory_meta.instantiate( 275 | step=step, 276 | args=args, 277 | kwargs=kwargs, 278 | ) 279 | 280 | postgen_results = {} 281 | for declaration_name in post.sorted(): 282 | declaration = post[declaration_name] 283 | postgen_results[declaration_name] = declaration.declaration.evaluate_post( 284 | instance=instance, 285 | step=step, 286 | overrides=declaration.context, 287 | ) 288 | self.factory_meta.use_postgeneration_results( 289 | instance=instance, 290 | step=step, 291 | results=postgen_results, 292 | ) 293 | return instance 294 | 295 | def recurse(self, factory_meta, extras): 296 | """Recurse into a sub-factory call.""" 297 | return self.__class__(factory_meta, extras, strategy=self.strategy) 298 | 299 | def __repr__(self): 300 | return f"" 301 | 302 | 303 | class Resolver: 304 | """Resolve a set of declarations. 305 | 306 | Attributes are set at instantiation time, values are computed lazily. 307 | 308 | Attributes: 309 | __initialized (bool): whether this object's __init__ as run. If set, 310 | setting any attribute will be prevented. 311 | __declarations (dict): maps attribute name to their declaration 312 | __values (dict): maps attribute name to computed value 313 | __pending (str list): names of the attributes whose value is being 314 | computed. This allows to detect cyclic lazy attribute definition. 315 | __step (BuildStep): the BuildStep related to this resolver. 316 | This allows to have the value of a field depend on the value of 317 | another field 318 | """ 319 | 320 | __initialized = False 321 | 322 | def __init__(self, declarations, step, sequence): 323 | self.__declarations = declarations 324 | self.__step = step 325 | 326 | self.__values = {} 327 | self.__pending = [] 328 | 329 | self.__initialized = True 330 | 331 | @property 332 | def factory_parent(self): 333 | return self.__step.parent_step.stub if self.__step.parent_step else None 334 | 335 | def __repr__(self): 336 | return '' % self.__step 337 | 338 | def __getattr__(self, name): 339 | """Retrieve an attribute's value. 340 | 341 | This will compute it if needed, unless it is already on the list of 342 | attributes being computed. 343 | """ 344 | if name in self.__pending: 345 | raise errors.CyclicDefinitionError( 346 | "Cyclic lazy attribute definition for %r; cycle found in %r." % 347 | (name, self.__pending)) 348 | elif name in self.__values: 349 | return self.__values[name] 350 | elif name in self.__declarations: 351 | declaration = self.__declarations[name] 352 | value = declaration.declaration 353 | if enums.get_builder_phase(value) == enums.BuilderPhase.ATTRIBUTE_RESOLUTION: 354 | self.__pending.append(name) 355 | try: 356 | value = value.evaluate_pre( 357 | instance=self, 358 | step=self.__step, 359 | overrides=declaration.context, 360 | ) 361 | finally: 362 | last = self.__pending.pop() 363 | assert name == last 364 | 365 | self.__values[name] = value 366 | return value 367 | else: 368 | raise AttributeError( 369 | "The parameter %r is unknown. Evaluated attributes are %r, " 370 | "definitions are %r." % (name, self.__values, self.__declarations)) 371 | 372 | def __setattr__(self, name, value): 373 | """Prevent setting attributes once __init__ is done.""" 374 | if not self.__initialized: 375 | return super().__setattr__(name, value) 376 | else: 377 | raise AttributeError('Setting of object attributes is not allowed') 378 | -------------------------------------------------------------------------------- /factory/django.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | """factory_boy extensions for use with the Django framework.""" 5 | 6 | 7 | import functools 8 | import io 9 | import logging 10 | import os 11 | import warnings 12 | from typing import Dict, TypeVar 13 | 14 | from django.contrib.auth.hashers import make_password 15 | from django.core import files as django_files 16 | from django.db import IntegrityError 17 | 18 | from . import base, declarations, errors 19 | 20 | logger = logging.getLogger('factory.generate') 21 | 22 | 23 | DEFAULT_DB_ALIAS = 'default' # Same as django.db.DEFAULT_DB_ALIAS 24 | T = TypeVar("T") 25 | 26 | _LAZY_LOADS: Dict[str, object] = {} 27 | 28 | 29 | def get_model(app, model): 30 | """Wrapper around django's get_model.""" 31 | if 'get_model' not in _LAZY_LOADS: 32 | _lazy_load_get_model() 33 | 34 | _get_model = _LAZY_LOADS['get_model'] 35 | return _get_model(app, model) 36 | 37 | 38 | def _lazy_load_get_model(): 39 | """Lazy loading of get_model. 40 | 41 | get_model loads django.conf.settings, which may fail if 42 | the settings haven't been configured yet. 43 | """ 44 | from django import apps as django_apps 45 | _LAZY_LOADS['get_model'] = django_apps.apps.get_model 46 | 47 | 48 | class DjangoOptions(base.FactoryOptions): 49 | def _build_default_options(self): 50 | return super()._build_default_options() + [ 51 | base.OptionDefault('django_get_or_create', (), inherit=True), 52 | base.OptionDefault('database', DEFAULT_DB_ALIAS, inherit=True), 53 | base.OptionDefault('skip_postgeneration_save', False, inherit=True), 54 | ] 55 | 56 | def _get_counter_reference(self): 57 | counter_reference = super()._get_counter_reference() 58 | if (counter_reference == self.base_factory 59 | and self.base_factory._meta.model is not None 60 | and self.base_factory._meta.model._meta.abstract 61 | and self.model is not None 62 | and not self.model._meta.abstract): 63 | # Target factory is for an abstract model, yet we're for another, 64 | # concrete subclass => don't reuse the counter. 65 | return self.factory 66 | return counter_reference 67 | 68 | def get_model_class(self): 69 | if isinstance(self.model, str) and '.' in self.model: 70 | app, model_name = self.model.split('.', 1) 71 | self.model = get_model(app, model_name) 72 | 73 | return self.model 74 | 75 | 76 | class DjangoModelFactory(base.Factory[T]): 77 | """Factory for Django models. 78 | 79 | This makes sure that the 'sequence' field of created objects is a new id. 80 | 81 | Possible improvement: define a new 'attribute' type, AutoField, which would 82 | handle those for non-numerical primary keys. 83 | """ 84 | 85 | _options_class = DjangoOptions 86 | _original_params = None 87 | 88 | class Meta: 89 | abstract = True # Optional, but explicit. 90 | 91 | @classmethod 92 | def _load_model_class(cls, definition): 93 | 94 | if isinstance(definition, str) and '.' in definition: 95 | app, model = definition.split('.', 1) 96 | return get_model(app, model) 97 | 98 | return definition 99 | 100 | @classmethod 101 | def _get_manager(cls, model_class): 102 | if model_class is None: 103 | raise errors.AssociatedClassError( 104 | f"No model set on {cls.__module__}.{cls.__name__}.Meta") 105 | 106 | try: 107 | manager = model_class.objects 108 | except AttributeError: 109 | # When inheriting from an abstract model with a custom 110 | # manager, the class has no 'objects' field. 111 | manager = model_class._default_manager 112 | 113 | if cls._meta.database != DEFAULT_DB_ALIAS: 114 | manager = manager.using(cls._meta.database) 115 | return manager 116 | 117 | @classmethod 118 | def _generate(cls, strategy, params): 119 | # Original params are used in _get_or_create if it cannot build an 120 | # object initially due to an IntegrityError being raised 121 | cls._original_params = params 122 | return super()._generate(strategy, params) 123 | 124 | @classmethod 125 | def _get_or_create(cls, model_class, *args, **kwargs): 126 | """Create an instance of the model through objects.get_or_create.""" 127 | manager = cls._get_manager(model_class) 128 | 129 | assert 'defaults' not in cls._meta.django_get_or_create, ( 130 | "'defaults' is a reserved keyword for get_or_create " 131 | "(in %s._meta.django_get_or_create=%r)" 132 | % (cls, cls._meta.django_get_or_create)) 133 | 134 | key_fields = {} 135 | for field in cls._meta.django_get_or_create: 136 | if field not in kwargs: 137 | raise errors.FactoryError( 138 | "django_get_or_create - " 139 | "Unable to find initialization value for '%s' in factory %s" % 140 | (field, cls.__name__)) 141 | key_fields[field] = kwargs.pop(field) 142 | key_fields['defaults'] = kwargs 143 | 144 | try: 145 | instance, _created = manager.get_or_create(*args, **key_fields) 146 | except IntegrityError as e: 147 | 148 | if cls._original_params is None: 149 | raise e 150 | 151 | get_or_create_params = { 152 | lookup: value 153 | for lookup, value in cls._original_params.items() 154 | if lookup in cls._meta.django_get_or_create 155 | } 156 | if get_or_create_params: 157 | try: 158 | instance = manager.get(**get_or_create_params) 159 | except manager.model.DoesNotExist: 160 | # Original params are not a valid lookup and triggered a create(), 161 | # that resulted in an IntegrityError. Follow Django’s behavior. 162 | raise e 163 | else: 164 | raise e 165 | 166 | return instance 167 | 168 | @classmethod 169 | def _create(cls, model_class, *args, **kwargs): 170 | """Create an instance of the model, and save it to the database.""" 171 | if cls._meta.django_get_or_create: 172 | return cls._get_or_create(model_class, *args, **kwargs) 173 | 174 | manager = cls._get_manager(model_class) 175 | return manager.create(*args, **kwargs) 176 | 177 | # DEPRECATED. Remove this override with the next major release. 178 | @classmethod 179 | def _after_postgeneration(cls, instance, create, results=None): 180 | """Save again the instance if creating and at least one hook ran.""" 181 | if create and results and not cls._meta.skip_postgeneration_save: 182 | warnings.warn( 183 | f"{cls.__name__}._after_postgeneration will stop saving the instance " 184 | "after postgeneration hooks in the next major release.\n" 185 | "If the save call is extraneous, set skip_postgeneration_save=True " 186 | f"in the {cls.__name__}.Meta.\n" 187 | "To keep saving the instance, move the save call to your " 188 | "postgeneration hooks or override _after_postgeneration.", 189 | DeprecationWarning, 190 | ) 191 | # Some post-generation hooks ran, and may have modified us. 192 | instance.save() 193 | 194 | 195 | class Password(declarations.Transformer): 196 | def __init__(self, password, transform=make_password, **kwargs): 197 | super().__init__(password, transform=transform, **kwargs) 198 | 199 | 200 | class FileField(declarations.BaseDeclaration): 201 | """Helper to fill in django.db.models.FileField from a Factory.""" 202 | 203 | DEFAULT_FILENAME = 'example.dat' 204 | 205 | def _make_data(self, params): 206 | """Create data for the field.""" 207 | return params.get('data', b'') 208 | 209 | def _make_content(self, params): 210 | path = '' 211 | 212 | from_path = params.get('from_path') 213 | from_file = params.get('from_file') 214 | from_func = params.get('from_func') 215 | 216 | if len([p for p in (from_path, from_file, from_func) if p]) > 1: 217 | raise ValueError( 218 | "At most one argument from 'from_file', 'from_path', and 'from_func' should " 219 | "be non-empty when calling factory.django.FileField." 220 | ) 221 | 222 | if from_path: 223 | path = from_path 224 | with open(path, 'rb') as f: 225 | content = django_files.base.ContentFile(f.read()) 226 | 227 | elif from_file: 228 | f = from_file 229 | content = django_files.File(f) 230 | path = content.name 231 | 232 | elif from_func: 233 | func = from_func 234 | content = django_files.File(func()) 235 | path = content.name 236 | 237 | else: 238 | data = self._make_data(params) 239 | content = django_files.base.ContentFile(data) 240 | 241 | if path: 242 | default_filename = os.path.basename(path) 243 | else: 244 | default_filename = self.DEFAULT_FILENAME 245 | 246 | filename = params.get('filename', default_filename) 247 | return filename, content 248 | 249 | def evaluate(self, instance, step, extra): 250 | """Fill in the field.""" 251 | filename, content = self._make_content(extra) 252 | return django_files.File(content.file, filename) 253 | 254 | 255 | class ImageField(FileField): 256 | DEFAULT_FILENAME = 'example.jpg' 257 | 258 | def _make_data(self, params): 259 | # ImageField (both django's and factory_boy's) require PIL. 260 | # Try to import it along one of its known installation paths. 261 | from PIL import Image 262 | 263 | width = params.get('width', 100) 264 | height = params.get('height', width) 265 | color = params.get('color', 'blue') 266 | image_format = params.get('format', 'JPEG') 267 | image_palette = params.get('palette', 'RGB') 268 | 269 | thumb_io = io.BytesIO() 270 | with Image.new(image_palette, (width, height), color) as thumb: 271 | thumb.save(thumb_io, format=image_format) 272 | return thumb_io.getvalue() 273 | 274 | 275 | class mute_signals: 276 | """Temporarily disables and then restores any django signals. 277 | 278 | Args: 279 | *signals (django.dispatch.dispatcher.Signal): any django signals 280 | 281 | Examples: 282 | with mute_signals(pre_init): 283 | user = UserFactory.build() 284 | ... 285 | 286 | @mute_signals(pre_save, post_save) 287 | class UserFactory(factory.Factory): 288 | ... 289 | 290 | @mute_signals(post_save) 291 | def generate_users(): 292 | UserFactory.create_batch(10) 293 | """ 294 | 295 | def __init__(self, *signals): 296 | self.signals = signals 297 | self.paused = {} 298 | 299 | def __enter__(self): 300 | for signal in self.signals: 301 | logger.debug('mute_signals: Disabling signal handlers %r', 302 | signal.receivers) 303 | 304 | # Note that we're using implementation details of 305 | # django.signals, since arguments to signal.connect() 306 | # are lost in signal.receivers 307 | self.paused[signal] = signal.receivers 308 | signal.receivers = [] 309 | 310 | def __exit__(self, exc_type, exc_value, traceback): 311 | for signal, receivers in self.paused.items(): 312 | logger.debug('mute_signals: Restoring signal handlers %r', 313 | receivers) 314 | 315 | signal.receivers = receivers + signal.receivers 316 | with signal.lock: 317 | # Django uses some caching for its signals. 318 | # Since we're bypassing signal.connect and signal.disconnect, 319 | # we have to keep messing with django's internals. 320 | signal.sender_receivers_cache.clear() 321 | self.paused = {} 322 | 323 | def copy(self): 324 | return mute_signals(*self.signals) 325 | 326 | def __call__(self, callable_obj): 327 | if isinstance(callable_obj, base.FactoryMetaClass): 328 | # Retrieve __func__, the *actual* callable object. 329 | callable_obj._create = self.wrap_method(callable_obj._create.__func__) 330 | callable_obj._generate = self.wrap_method(callable_obj._generate.__func__) 331 | callable_obj._after_postgeneration = self.wrap_method( 332 | callable_obj._after_postgeneration.__func__ 333 | ) 334 | return callable_obj 335 | 336 | else: 337 | @functools.wraps(callable_obj) 338 | def wrapper(*args, **kwargs): 339 | # A mute_signals() object is not reentrant; use a copy every time. 340 | with self.copy(): 341 | return callable_obj(*args, **kwargs) 342 | return wrapper 343 | 344 | def wrap_method(self, method): 345 | @classmethod 346 | @functools.wraps(method) 347 | def wrapped_method(*args, **kwargs): 348 | # A mute_signals() object is not reentrant; use a copy every time. 349 | with self.copy(): 350 | return method(*args, **kwargs) 351 | return wrapped_method 352 | -------------------------------------------------------------------------------- /factory/enums.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | # Strategies 4 | BUILD_STRATEGY = 'build' 5 | CREATE_STRATEGY = 'create' 6 | STUB_STRATEGY = 'stub' 7 | 8 | 9 | #: String for splitting an attribute name into a 10 | #: (subfactory_name, subfactory_field) tuple. 11 | SPLITTER = '__' 12 | 13 | 14 | # Target build phase, for declarations 15 | class BuilderPhase: 16 | #: During attribute resolution/computation 17 | ATTRIBUTE_RESOLUTION = 'attributes' 18 | 19 | #: Once the target object has been built 20 | POST_INSTANTIATION = 'post_instance' 21 | 22 | 23 | def get_builder_phase(obj): 24 | return getattr(obj, 'FACTORY_BUILDER_PHASE', None) 25 | -------------------------------------------------------------------------------- /factory/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | class FactoryError(Exception): 5 | """Any exception raised by factory_boy.""" 6 | 7 | 8 | class AssociatedClassError(FactoryError): 9 | """Exception for Factory subclasses lacking Meta.model.""" 10 | 11 | 12 | class UnknownStrategy(FactoryError): 13 | """Raised when a factory uses an unknown strategy.""" 14 | 15 | 16 | class UnsupportedStrategy(FactoryError): 17 | """Raised when trying to use a strategy on an incompatible Factory.""" 18 | 19 | 20 | class CyclicDefinitionError(FactoryError): 21 | """Raised when a cyclical declaration occurs.""" 22 | 23 | 24 | class InvalidDeclarationError(FactoryError): 25 | """Raised when a sub-declaration has no related declaration. 26 | 27 | This means that the user declared 'foo__bar' without adding a declaration 28 | at 'foo'. 29 | """ 30 | -------------------------------------------------------------------------------- /factory/faker.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | """Additional declarations for "faker" attributes. 5 | 6 | Usage: 7 | 8 | class MyFactory(factory.Factory): 9 | class Meta: 10 | model = MyProfile 11 | 12 | first_name = factory.Faker('name') 13 | """ 14 | 15 | 16 | import contextlib 17 | from typing import Dict 18 | 19 | import faker 20 | import faker.config 21 | 22 | from . import declarations 23 | 24 | 25 | class Faker(declarations.BaseDeclaration): 26 | """Wrapper for 'faker' values. 27 | 28 | Args: 29 | provider (str): the name of the Faker field 30 | locale (str): the locale to use for the faker 31 | 32 | All other kwargs will be passed to the underlying provider 33 | (e.g ``factory.Faker('ean', length=10)`` 34 | calls ``faker.Faker.ean(length=10)``) 35 | 36 | Usage: 37 | >>> foo = factory.Faker('name') 38 | """ 39 | def __init__(self, provider, **kwargs): 40 | locale = kwargs.pop('locale', None) 41 | self.provider = provider 42 | super().__init__( 43 | locale=locale, 44 | **kwargs) 45 | 46 | def evaluate(self, instance, step, extra): 47 | locale = extra.pop('locale') 48 | subfaker = self._get_faker(locale) 49 | return subfaker.format(self.provider, **extra) 50 | 51 | _FAKER_REGISTRY: Dict[str, faker.Faker] = {} 52 | _DEFAULT_LOCALE = faker.config.DEFAULT_LOCALE 53 | 54 | @classmethod 55 | @contextlib.contextmanager 56 | def override_default_locale(cls, locale): 57 | old_locale = cls._DEFAULT_LOCALE 58 | cls._DEFAULT_LOCALE = locale 59 | try: 60 | yield 61 | finally: 62 | cls._DEFAULT_LOCALE = old_locale 63 | 64 | @classmethod 65 | def _get_faker(cls, locale=None): 66 | if locale is None: 67 | locale = cls._DEFAULT_LOCALE 68 | 69 | if locale not in cls._FAKER_REGISTRY: 70 | subfaker = faker.Faker(locale=locale) 71 | cls._FAKER_REGISTRY[locale] = subfaker 72 | 73 | return cls._FAKER_REGISTRY[locale] 74 | 75 | @classmethod 76 | def add_provider(cls, provider, locale=None): 77 | """Add a new Faker provider for the specified locale""" 78 | cls._get_faker(locale).add_provider(provider) 79 | -------------------------------------------------------------------------------- /factory/fuzzy.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | """Additional declarations for "fuzzy" attribute definitions.""" 5 | 6 | 7 | import datetime 8 | import decimal 9 | import string 10 | import warnings 11 | 12 | from . import declarations, random 13 | 14 | random_seed_warning = ( 15 | "Setting a specific random seed for {} can still have varying results " 16 | "unless you also set a specific end date. For details and potential solutions " 17 | "see https://github.com/FactoryBoy/factory_boy/issues/331" 18 | ) 19 | 20 | 21 | class BaseFuzzyAttribute(declarations.BaseDeclaration): 22 | """Base class for fuzzy attributes. 23 | 24 | Custom fuzzers should override the `fuzz()` method. 25 | """ 26 | 27 | def fuzz(self): # pragma: no cover 28 | raise NotImplementedError() 29 | 30 | def evaluate(self, instance, step, extra): 31 | return self.fuzz() 32 | 33 | 34 | class FuzzyAttribute(BaseFuzzyAttribute): 35 | """Similar to LazyAttribute, but yields random values. 36 | 37 | Attributes: 38 | function (callable): function taking no parameters and returning a 39 | random value. 40 | """ 41 | 42 | def __init__(self, fuzzer): 43 | super().__init__() 44 | self.fuzzer = fuzzer 45 | 46 | def fuzz(self): 47 | return self.fuzzer() 48 | 49 | 50 | class FuzzyText(BaseFuzzyAttribute): 51 | """Random string with a given prefix. 52 | 53 | Generates a random string of the given length from chosen chars. 54 | If a prefix or a suffix are supplied, they will be prepended / appended 55 | to the generated string. 56 | 57 | Args: 58 | prefix (text): An optional prefix to prepend to the random string 59 | length (int): the length of the random part 60 | suffix (text): An optional suffix to append to the random string 61 | chars (str list): the chars to choose from 62 | 63 | Useful for generating unique attributes where the exact value is 64 | not important. 65 | """ 66 | 67 | def __init__(self, prefix='', length=12, suffix='', chars=string.ascii_letters): 68 | super().__init__() 69 | self.prefix = prefix 70 | self.suffix = suffix 71 | self.length = length 72 | self.chars = tuple(chars) # Unroll iterators 73 | 74 | def fuzz(self): 75 | chars = [random.randgen.choice(self.chars) for _i in range(self.length)] 76 | return self.prefix + ''.join(chars) + self.suffix 77 | 78 | 79 | class FuzzyChoice(BaseFuzzyAttribute): 80 | """Handles fuzzy choice of an attribute. 81 | 82 | Args: 83 | choices (iterable): An iterable yielding options; will only be unrolled 84 | on the first call. 85 | getter (callable or None): a function to parse returned values 86 | """ 87 | 88 | def __init__(self, choices, getter=None): 89 | self.choices = None 90 | self.choices_generator = choices 91 | self.getter = getter 92 | super().__init__() 93 | 94 | def fuzz(self): 95 | if self.choices is None: 96 | self.choices = list(self.choices_generator) 97 | value = random.randgen.choice(self.choices) 98 | if self.getter is None: 99 | return value 100 | return self.getter(value) 101 | 102 | 103 | class FuzzyInteger(BaseFuzzyAttribute): 104 | """Random integer within a given range.""" 105 | 106 | def __init__(self, low, high=None, step=1): 107 | if high is None: 108 | high = low 109 | low = 0 110 | 111 | self.low = low 112 | self.high = high 113 | self.step = step 114 | 115 | super().__init__() 116 | 117 | def fuzz(self): 118 | return random.randgen.randrange(self.low, self.high + 1, self.step) 119 | 120 | 121 | class FuzzyDecimal(BaseFuzzyAttribute): 122 | """Random decimal within a given range.""" 123 | 124 | def __init__(self, low, high=None, precision=2): 125 | if high is None: 126 | high = low 127 | low = 0.0 128 | 129 | self.low = low 130 | self.high = high 131 | self.precision = precision 132 | 133 | super().__init__() 134 | 135 | def fuzz(self): 136 | base = decimal.Decimal(str(random.randgen.uniform(self.low, self.high))) 137 | return base.quantize(decimal.Decimal(10) ** -self.precision) 138 | 139 | 140 | class FuzzyFloat(BaseFuzzyAttribute): 141 | """Random float within a given range.""" 142 | 143 | def __init__(self, low, high=None, precision=15): 144 | if high is None: 145 | high = low 146 | low = 0 147 | 148 | self.low = low 149 | self.high = high 150 | self.precision = precision 151 | 152 | super().__init__() 153 | 154 | def fuzz(self): 155 | base = random.randgen.uniform(self.low, self.high) 156 | return float(format(base, '.%dg' % self.precision)) 157 | 158 | 159 | class FuzzyDate(BaseFuzzyAttribute): 160 | """Random date within a given date range.""" 161 | 162 | def __init__(self, start_date, end_date=None): 163 | super().__init__() 164 | if end_date is None: 165 | if random.randgen.state_set: 166 | cls_name = self.__class__.__name__ 167 | warnings.warn(random_seed_warning.format(cls_name), stacklevel=2) 168 | end_date = datetime.date.today() 169 | 170 | if start_date > end_date: 171 | raise ValueError( 172 | "FuzzyDate boundaries should have start <= end; got %r > %r." 173 | % (start_date, end_date)) 174 | 175 | self.start_date = start_date.toordinal() 176 | self.end_date = end_date.toordinal() 177 | 178 | def fuzz(self): 179 | return datetime.date.fromordinal(random.randgen.randint(self.start_date, self.end_date)) 180 | 181 | 182 | class BaseFuzzyDateTime(BaseFuzzyAttribute): 183 | """Base class for fuzzy datetime-related attributes. 184 | 185 | Provides fuzz() computation, forcing year/month/day/hour/... 186 | """ 187 | 188 | def _check_bounds(self, start_dt, end_dt): 189 | if start_dt > end_dt: 190 | raise ValueError( 191 | """%s boundaries should have start <= end, got %r > %r""" % ( 192 | self.__class__.__name__, start_dt, end_dt)) 193 | 194 | def _now(self): 195 | raise NotImplementedError() 196 | 197 | def __init__(self, start_dt, end_dt=None, 198 | force_year=None, force_month=None, force_day=None, 199 | force_hour=None, force_minute=None, force_second=None, 200 | force_microsecond=None): 201 | super().__init__() 202 | 203 | if end_dt is None: 204 | if random.randgen.state_set: 205 | cls_name = self.__class__.__name__ 206 | warnings.warn(random_seed_warning.format(cls_name), stacklevel=2) 207 | end_dt = self._now() 208 | 209 | self._check_bounds(start_dt, end_dt) 210 | 211 | self.start_dt = start_dt 212 | self.end_dt = end_dt 213 | self.force_year = force_year 214 | self.force_month = force_month 215 | self.force_day = force_day 216 | self.force_hour = force_hour 217 | self.force_minute = force_minute 218 | self.force_second = force_second 219 | self.force_microsecond = force_microsecond 220 | 221 | def fuzz(self): 222 | delta = self.end_dt - self.start_dt 223 | microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400)) 224 | 225 | offset = random.randgen.randint(0, microseconds) 226 | result = self.start_dt + datetime.timedelta(microseconds=offset) 227 | 228 | if self.force_year is not None: 229 | result = result.replace(year=self.force_year) 230 | if self.force_month is not None: 231 | result = result.replace(month=self.force_month) 232 | if self.force_day is not None: 233 | result = result.replace(day=self.force_day) 234 | if self.force_hour is not None: 235 | result = result.replace(hour=self.force_hour) 236 | if self.force_minute is not None: 237 | result = result.replace(minute=self.force_minute) 238 | if self.force_second is not None: 239 | result = result.replace(second=self.force_second) 240 | if self.force_microsecond is not None: 241 | result = result.replace(microsecond=self.force_microsecond) 242 | 243 | return result 244 | 245 | 246 | class FuzzyNaiveDateTime(BaseFuzzyDateTime): 247 | """Random naive datetime within a given range. 248 | 249 | If no upper bound is given, will default to datetime.datetime.now(). 250 | """ 251 | 252 | def _now(self): 253 | return datetime.datetime.now() 254 | 255 | def _check_bounds(self, start_dt, end_dt): 256 | if start_dt.tzinfo is not None: 257 | raise ValueError( 258 | "FuzzyNaiveDateTime only handles naive datetimes, got start=%r" 259 | % start_dt) 260 | if end_dt.tzinfo is not None: 261 | raise ValueError( 262 | "FuzzyNaiveDateTime only handles naive datetimes, got end=%r" 263 | % end_dt) 264 | super()._check_bounds(start_dt, end_dt) 265 | 266 | 267 | class FuzzyDateTime(BaseFuzzyDateTime): 268 | """Random timezone-aware datetime within a given range. 269 | 270 | If no upper bound is given, will default to datetime.datetime.now() 271 | If no timezone is given, will default to utc. 272 | """ 273 | 274 | def _now(self): 275 | return datetime.datetime.now(tz=datetime.timezone.utc) 276 | 277 | def _check_bounds(self, start_dt, end_dt): 278 | if start_dt.tzinfo is None: 279 | raise ValueError( 280 | "FuzzyDateTime requires timezone-aware datetimes, got start=%r" 281 | % start_dt) 282 | if end_dt.tzinfo is None: 283 | raise ValueError( 284 | "FuzzyDateTime requires timezone-aware datetimes, got end=%r" 285 | % end_dt) 286 | super()._check_bounds(start_dt, end_dt) 287 | -------------------------------------------------------------------------------- /factory/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | """Simple wrappers around Factory class definition.""" 5 | 6 | import contextlib 7 | import logging 8 | 9 | from . import base, declarations 10 | 11 | 12 | @contextlib.contextmanager 13 | def debug(logger='factory', stream=None): 14 | logger_obj = logging.getLogger(logger) 15 | old_level = logger_obj.level 16 | 17 | handler = logging.StreamHandler(stream) 18 | handler.setLevel(logging.DEBUG) 19 | logger_obj.addHandler(handler) 20 | logger_obj.setLevel(logging.DEBUG) 21 | 22 | try: 23 | yield 24 | finally: 25 | logger_obj.setLevel(old_level) 26 | logger_obj.removeHandler(handler) 27 | 28 | 29 | def make_factory(klass, **kwargs): 30 | """Create a new, simple factory for the given class.""" 31 | factory_name = '%sFactory' % klass.__name__ 32 | 33 | class Meta: 34 | model = klass 35 | 36 | kwargs['Meta'] = Meta 37 | base_class = kwargs.pop('FACTORY_CLASS', base.Factory) 38 | 39 | factory_class = type(base.Factory).__new__(type(base.Factory), factory_name, (base_class,), kwargs) 40 | factory_class.__name__ = '%sFactory' % klass.__name__ 41 | factory_class.__doc__ = 'Auto-generated factory for class %s' % klass 42 | return factory_class 43 | 44 | 45 | def build(klass, **kwargs): 46 | """Create a factory for the given class, and build an instance.""" 47 | return make_factory(klass, **kwargs).build() 48 | 49 | 50 | def build_batch(klass, size, **kwargs): 51 | """Create a factory for the given class, and build a batch of instances.""" 52 | return make_factory(klass, **kwargs).build_batch(size) 53 | 54 | 55 | def create(klass, **kwargs): 56 | """Create a factory for the given class, and create an instance.""" 57 | return make_factory(klass, **kwargs).create() 58 | 59 | 60 | def create_batch(klass, size, **kwargs): 61 | """Create a factory for the given class, and create a batch of instances.""" 62 | return make_factory(klass, **kwargs).create_batch(size) 63 | 64 | 65 | def stub(klass, **kwargs): 66 | """Create a factory for the given class, and stub an instance.""" 67 | return make_factory(klass, **kwargs).stub() 68 | 69 | 70 | def stub_batch(klass, size, **kwargs): 71 | """Create a factory for the given class, and stub a batch of instances.""" 72 | return make_factory(klass, **kwargs).stub_batch(size) 73 | 74 | 75 | def generate(klass, strategy, **kwargs): 76 | """Create a factory for the given class, and generate an instance.""" 77 | return make_factory(klass, **kwargs).generate(strategy) 78 | 79 | 80 | def generate_batch(klass, strategy, size, **kwargs): 81 | """Create a factory for the given class, and generate instances.""" 82 | return make_factory(klass, **kwargs).generate_batch(strategy, size) 83 | 84 | 85 | def simple_generate(klass, create, **kwargs): 86 | """Create a factory for the given class, and simple_generate an instance.""" 87 | return make_factory(klass, **kwargs).simple_generate(create) 88 | 89 | 90 | def simple_generate_batch(klass, create, size, **kwargs): 91 | """Create a factory for the given class, and simple_generate instances.""" 92 | return make_factory(klass, **kwargs).simple_generate_batch(create, size) 93 | 94 | 95 | def lazy_attribute(func): 96 | return declarations.LazyAttribute(func) 97 | 98 | 99 | def iterator(func): 100 | """Turn a generator function into an iterator attribute.""" 101 | return declarations.Iterator(func()) 102 | 103 | 104 | def sequence(func): 105 | return declarations.Sequence(func) 106 | 107 | 108 | def lazy_attribute_sequence(func): 109 | return declarations.LazyAttributeSequence(func) 110 | 111 | 112 | def container_attribute(func): 113 | return declarations.ContainerAttribute(func, strict=False) 114 | 115 | 116 | def post_generation(fun): 117 | return declarations.PostGeneration(fun) 118 | -------------------------------------------------------------------------------- /factory/mogo.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | """factory_boy extensions for use with the mogo library (pymongo wrapper).""" 5 | 6 | 7 | from . import base 8 | 9 | 10 | class MogoFactory(base.Factory): 11 | """Factory for mogo objects.""" 12 | class Meta: 13 | abstract = True 14 | 15 | @classmethod 16 | def _build(cls, model_class, *args, **kwargs): 17 | return model_class(*args, **kwargs) 18 | 19 | @classmethod 20 | def _create(cls, model_class, *args, **kwargs): 21 | instance = model_class(*args, **kwargs) 22 | instance.save() 23 | return instance 24 | -------------------------------------------------------------------------------- /factory/mongoengine.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | """factory_boy extensions for use with the mongoengine library (pymongo wrapper).""" 5 | 6 | 7 | from . import base 8 | 9 | 10 | class MongoEngineFactory(base.Factory): 11 | """Factory for mongoengine objects.""" 12 | 13 | class Meta: 14 | abstract = True 15 | 16 | @classmethod 17 | def _build(cls, model_class, *args, **kwargs): 18 | return model_class(*args, **kwargs) 19 | 20 | @classmethod 21 | def _create(cls, model_class, *args, **kwargs): 22 | instance = model_class(*args, **kwargs) 23 | if instance._is_document: 24 | instance.save() 25 | return instance 26 | -------------------------------------------------------------------------------- /factory/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FactoryBoy/factory_boy/68feb45e182f9acccfde671b7ba0babb5bc7ce11/factory/py.typed -------------------------------------------------------------------------------- /factory/random.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import faker.generator 4 | 5 | randgen = random.Random() 6 | 7 | randgen.state_set = False 8 | 9 | 10 | def get_random_state(): 11 | """Retrieve the state of factory.fuzzy's random generator.""" 12 | state = randgen.getstate() 13 | # Returned state must represent both Faker and factory_boy. 14 | faker.generator.random.setstate(state) 15 | return state 16 | 17 | 18 | def set_random_state(state): 19 | """Force-set the state of factory.fuzzy's random generator.""" 20 | randgen.state_set = True 21 | randgen.setstate(state) 22 | 23 | faker.generator.random.setstate(state) 24 | 25 | 26 | def reseed_random(seed): 27 | """Reseed factory.fuzzy's random generator.""" 28 | r = random.Random(seed) 29 | random_internal_state = r.getstate() 30 | set_random_state(random_internal_state) 31 | -------------------------------------------------------------------------------- /factory/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | import collections 5 | import importlib 6 | 7 | 8 | def import_object(module_name, attribute_name): 9 | """Import an object from its absolute path. 10 | 11 | Example: 12 | >>> import_object('datetime', 'datetime') 13 | 14 | """ 15 | module = importlib.import_module(module_name) 16 | return getattr(module, attribute_name) 17 | 18 | 19 | class log_pprint: 20 | """Helper for properly printing args / kwargs passed to an object. 21 | 22 | Since it is only used with factory.debug(), the computation is 23 | performed lazily. 24 | """ 25 | __slots__ = ['args', 'kwargs'] 26 | 27 | def __init__(self, args=(), kwargs=None): 28 | self.args = args 29 | self.kwargs = kwargs or {} 30 | 31 | def __repr__(self): 32 | return repr(str(self)) 33 | 34 | def __str__(self): 35 | return ', '.join( 36 | [ 37 | repr(arg) for arg in self.args 38 | ] + [ 39 | '%s=%s' % (key, repr(value)) 40 | for key, value in self.kwargs.items() 41 | ] 42 | ) 43 | 44 | 45 | class ResetableIterator: 46 | """An iterator wrapper that can be 'reset()' to its start.""" 47 | def __init__(self, iterator, **kwargs): 48 | super().__init__(**kwargs) 49 | self.iterator = iter(iterator) 50 | self.past_elements = collections.deque() 51 | self.next_elements = collections.deque() 52 | 53 | def __iter__(self): 54 | while True: 55 | if self.next_elements: 56 | yield self.next_elements.popleft() 57 | else: 58 | try: 59 | value = next(self.iterator) 60 | except StopIteration: 61 | break 62 | else: 63 | self.past_elements.append(value) 64 | yield value 65 | 66 | def reset(self): 67 | self.next_elements.clear() 68 | self.next_elements.extend(self.past_elements) 69 | 70 | 71 | class OrderedBase: 72 | """Marks a class as being ordered. 73 | 74 | Each instance (even from subclasses) will share a global creation counter. 75 | """ 76 | 77 | CREATION_COUNTER_FIELD = '_creation_counter' 78 | 79 | def __init__(self, **kwargs): 80 | super().__init__(**kwargs) 81 | if type(self) is not OrderedBase: 82 | self.touch_creation_counter() 83 | 84 | def touch_creation_counter(self): 85 | bases = type(self).__mro__ 86 | root = bases[bases.index(OrderedBase) - 1] 87 | if not hasattr(root, self.CREATION_COUNTER_FIELD): 88 | setattr(root, self.CREATION_COUNTER_FIELD, 0) 89 | next_counter = getattr(root, self.CREATION_COUNTER_FIELD) 90 | setattr(self, self.CREATION_COUNTER_FIELD, next_counter) 91 | setattr(root, self.CREATION_COUNTER_FIELD, next_counter + 1) 92 | 93 | 94 | def sort_ordered_objects(items, getter=lambda x: x): 95 | """Sort an iterable of OrderedBase instances. 96 | 97 | Args: 98 | items (iterable): the objects to sort 99 | getter (callable or None): a function to extract the OrderedBase instance from an object. 100 | 101 | Examples: 102 | >>> sort_ordered_objects([x, y, z]) 103 | >>> sort_ordered_objects(v.items(), getter=lambda e: e[1]) 104 | """ 105 | return sorted(items, key=lambda x: getattr(getter(x), OrderedBase.CREATION_COUNTER_FIELD, -1)) 106 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = factory_boy 3 | version = 3.3.4.dev0 4 | description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. 5 | long_description = file: README.rst 6 | # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data 7 | long_description_content_type = text/x-rst 8 | author = Mark Sandstrom 9 | author_email = mark@deliciouslynerdy.com 10 | maintainer = Raphaël Barrois 11 | maintainer_email = raphael.barrois+fboy@polytechnique.org 12 | url = https://github.com/FactoryBoy/factory_boy 13 | keywords = factory_boy, factory, fixtures 14 | license = MIT 15 | classifiers = 16 | Development Status :: 5 - Production/Stable 17 | Framework :: Django 18 | Framework :: Django :: 4.2 19 | Framework :: Django :: 5.0 20 | Framework :: Django :: 5.1 21 | Intended Audience :: Developers 22 | License :: OSI Approved :: MIT License 23 | Operating System :: OS Independent 24 | Programming Language :: Python 25 | Programming Language :: Python :: 3 26 | Programming Language :: Python :: 3 :: Only 27 | Programming Language :: Python :: 3.9 28 | Programming Language :: Python :: 3.10 29 | Programming Language :: Python :: 3.11 30 | Programming Language :: Python :: 3.12 31 | Programming Language :: Python :: 3.13 32 | Programming Language :: Python :: Implementation :: CPython 33 | Programming Language :: Python :: Implementation :: PyPy 34 | Topic :: Software Development :: Testing 35 | Topic :: Software Development :: Libraries :: Python Modules 36 | 37 | [options] 38 | packages = factory 39 | python_requires = >=3.8 40 | install_requires = 41 | Faker>=0.7.0 42 | 43 | [options.extras_require] 44 | dev = 45 | coverage 46 | Django 47 | flake8 48 | isort 49 | mypy 50 | Pillow 51 | SQLAlchemy 52 | mongoengine 53 | mongomock 54 | wheel>=0.32.0 55 | tox 56 | zest.releaser[recommended] 57 | doc = 58 | Sphinx 59 | sphinx_rtd_theme 60 | sphinxcontrib-spelling 61 | 62 | [options.package_data] 63 | factory = 64 | py.typed 65 | 66 | [bdist_wheel] 67 | universal = 1 68 | 69 | [zest.releaser] 70 | ; semver-style versions 71 | version-levels = 3 72 | 73 | [distutils] 74 | index-servers = pypi 75 | 76 | [flake8] 77 | ignore = 78 | # Ignore "and" at start of line. 79 | W503 80 | # Ignore "do not assign a lambda expression, use a def". 81 | E731 82 | max-line-length = 120 83 | 84 | [isort] 85 | multi_line_output = 3 86 | include_trailing_comma = True 87 | force_grid_wrap = 0 88 | use_parentheses = True 89 | line_length = 88 90 | 91 | [coverage:run] 92 | dynamic_context = test_function 93 | 94 | [coverage:report] 95 | include= 96 | factory/*.py 97 | tests/*.py 98 | 99 | [coverage:html] 100 | show_contexts = True 101 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup() 6 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FactoryBoy/factory_boy/68feb45e182f9acccfde671b7ba0babb5bc7ce11/tests/__init__.py -------------------------------------------------------------------------------- /tests/alchemyapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FactoryBoy/factory_boy/68feb45e182f9acccfde671b7ba0babb5bc7ce11/tests/alchemyapp/__init__.py -------------------------------------------------------------------------------- /tests/alchemyapp/models.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | """Helpers for testing SQLAlchemy apps.""" 5 | 6 | from sqlalchemy import Column, Integer, Unicode, create_engine 7 | from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker 8 | 9 | engine_name = 'sqlite://' 10 | 11 | session = scoped_session(sessionmaker()) 12 | engine = create_engine(engine_name) 13 | session.configure(bind=engine) 14 | Base = declarative_base() 15 | 16 | 17 | class StandardModel(Base): 18 | __tablename__ = 'StandardModelTable' 19 | 20 | id = Column(Integer(), primary_key=True) 21 | foo = Column(Unicode(20)) 22 | 23 | 24 | class MultiFieldModel(Base): 25 | __tablename__ = 'MultiFieldModelTable' 26 | 27 | id = Column(Integer(), primary_key=True) 28 | foo = Column(Unicode(20)) 29 | slug = Column(Unicode(20), unique=True) 30 | 31 | 32 | class MultifieldUniqueModel(Base): 33 | __tablename__ = 'MultiFieldUniqueModelTable' 34 | 35 | id = Column(Integer(), primary_key=True) 36 | slug = Column(Unicode(20), unique=True) 37 | text = Column(Unicode(20), unique=True) 38 | title = Column(Unicode(20), unique=True) 39 | 40 | 41 | class NonIntegerPk(Base): 42 | __tablename__ = 'NonIntegerPk' 43 | 44 | id = Column(Unicode(20), primary_key=True) 45 | 46 | 47 | class SpecialFieldModel(Base): 48 | __tablename__ = 'SpecialFieldModelTable' 49 | 50 | id = Column(Integer(), primary_key=True) 51 | session = Column(Unicode(20)) 52 | -------------------------------------------------------------------------------- /tests/alter_time.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This code is in the public domain 3 | # Author: Raphaël Barrois 4 | 5 | 6 | import datetime 7 | from unittest import mock 8 | 9 | real_datetime_class = datetime.datetime 10 | 11 | 12 | def mock_datetime_now(target, datetime_module): 13 | """Override ``datetime.datetime.now()`` with a custom target value. 14 | 15 | This creates a new datetime.datetime class, and alters its now()/utcnow() 16 | methods. 17 | 18 | Returns: 19 | A mock.patch context, can be used as a decorator or in a with. 20 | """ 21 | 22 | # See https://bugs.python.org/msg68532 23 | # And https://docs.python.org/3/reference/datamodel.html#customizing-instance-and-subclass-checks 24 | class DatetimeSubclassMeta(type): 25 | """We need to customize the __instancecheck__ method for isinstance(). 26 | 27 | This must be performed at a metaclass level. 28 | """ 29 | 30 | @classmethod 31 | def __instancecheck__(mcs, obj): 32 | return isinstance(obj, real_datetime_class) 33 | 34 | class MockedDatetime(real_datetime_class, metaclass=DatetimeSubclassMeta): 35 | @classmethod 36 | def now(cls, tz=None): 37 | return target.replace(tzinfo=tz) 38 | 39 | @classmethod 40 | def utcnow(cls): 41 | return target 42 | 43 | return mock.patch.object(datetime_module, 'datetime', MockedDatetime) 44 | 45 | 46 | real_date_class = datetime.date 47 | 48 | 49 | def mock_date_today(target, datetime_module): 50 | """Override ``datetime.date.today()`` with a custom target value. 51 | 52 | This creates a new datetime.date class, and alters its today() method. 53 | 54 | Returns: 55 | A mock.patch context, can be used as a decorator or in a with. 56 | """ 57 | 58 | # See https://bugs.python.org/msg68532 59 | # And https://docs.python.org/3/reference/datamodel.html#customizing-instance-and-subclass-checks 60 | class DateSubclassMeta(type): 61 | """We need to customize the __instancecheck__ method for isinstance(). 62 | 63 | This must be performed at a metaclass level. 64 | """ 65 | 66 | @classmethod 67 | def __instancecheck__(mcs, obj): 68 | return isinstance(obj, real_date_class) 69 | 70 | class MockedDate(real_date_class, metaclass=DateSubclassMeta): 71 | @classmethod 72 | def today(cls): 73 | return target 74 | 75 | return mock.patch.object(datetime_module, 'date', MockedDate) 76 | 77 | 78 | def main(): # pragma: no cover 79 | """Run a couple of tests""" 80 | target_dt = real_datetime_class(2009, 1, 1) 81 | target_date = real_date_class(2009, 1, 1) 82 | 83 | print("Entering mock") 84 | with mock_datetime_now(target_dt, datetime): 85 | print("- now ->", datetime.datetime.now()) 86 | print("- isinstance(now, dt) ->", isinstance(datetime.datetime.now(), datetime.datetime)) 87 | print("- isinstance(target, dt) ->", isinstance(target_dt, datetime.datetime)) 88 | 89 | with mock_date_today(target_date, datetime): 90 | print("- today ->", datetime.date.today()) 91 | print("- isinstance(now, date) ->", isinstance(datetime.date.today(), datetime.date)) 92 | print("- isinstance(target, date) ->", isinstance(target_date, datetime.date)) 93 | 94 | print("Outside mock") 95 | print("- now ->", datetime.datetime.now()) 96 | print("- isinstance(now, dt) ->", isinstance(datetime.datetime.now(), datetime.datetime)) 97 | print("- isinstance(target, dt) ->", isinstance(target_dt, datetime.datetime)) 98 | 99 | print("- today ->", datetime.date.today()) 100 | print("- isinstance(now, date) ->", isinstance(datetime.date.today(), datetime.date)) 101 | print("- isinstance(target, date) ->", isinstance(target_date, datetime.date)) 102 | -------------------------------------------------------------------------------- /tests/cyclic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FactoryBoy/factory_boy/68feb45e182f9acccfde671b7ba0babb5bc7ce11/tests/cyclic/__init__.py -------------------------------------------------------------------------------- /tests/cyclic/bar.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | """Helper to test circular factory dependencies.""" 4 | 5 | import factory 6 | 7 | 8 | class Bar: 9 | def __init__(self, foo, y): 10 | self.foo = foo 11 | self.y = y 12 | 13 | 14 | class BarFactory(factory.Factory): 15 | class Meta: 16 | model = Bar 17 | 18 | y = 13 19 | foo = factory.SubFactory('cyclic.foo.FooFactory') 20 | -------------------------------------------------------------------------------- /tests/cyclic/foo.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | """Helper to test circular factory dependencies.""" 4 | 5 | import factory 6 | 7 | from . import bar as bar_mod 8 | 9 | 10 | class Foo: 11 | def __init__(self, bar, x): 12 | self.bar = bar 13 | self.x = x 14 | 15 | 16 | class FooFactory(factory.Factory): 17 | class Meta: 18 | model = Foo 19 | 20 | x = 42 21 | bar = factory.SubFactory(bar_mod.BarFactory) 22 | -------------------------------------------------------------------------------- /tests/cyclic/self_ref.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | """Helper to test circular factory dependencies.""" 4 | 5 | import factory 6 | 7 | 8 | class TreeElement: 9 | def __init__(self, name, parent): 10 | self.parent = parent 11 | self.name = name 12 | 13 | 14 | class TreeElementFactory(factory.Factory): 15 | class Meta: 16 | model = TreeElement 17 | 18 | name = factory.Sequence(lambda n: "tree%s" % n) 19 | parent = factory.SubFactory('tests.cyclic.self_ref.TreeElementFactory') 20 | -------------------------------------------------------------------------------- /tests/djapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FactoryBoy/factory_boy/68feb45e182f9acccfde671b7ba0babb5bc7ce11/tests/djapp/__init__.py -------------------------------------------------------------------------------- /tests/djapp/models.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | """Helpers for testing django apps.""" 4 | 5 | import os.path 6 | 7 | from django.conf import settings 8 | from django.db import models 9 | from django.db.models import signals 10 | 11 | try: 12 | from PIL import Image 13 | except ImportError: 14 | Image = None 15 | 16 | 17 | class StandardModel(models.Model): 18 | foo = models.CharField(max_length=20) 19 | 20 | 21 | class NonIntegerPk(models.Model): 22 | foo = models.CharField(max_length=20, primary_key=True) 23 | bar = models.CharField(max_length=20, blank=True) 24 | 25 | 26 | class MultifieldModel(models.Model): 27 | slug = models.SlugField(max_length=20, unique=True) 28 | text = models.TextField() 29 | 30 | 31 | class MultifieldUniqueModel(models.Model): 32 | slug = models.SlugField(max_length=20, unique=True) 33 | text = models.CharField(max_length=20, unique=True) 34 | title = models.CharField(max_length=20, unique=True) 35 | 36 | 37 | class AbstractBase(models.Model): 38 | foo = models.CharField(max_length=20) 39 | 40 | class Meta: 41 | abstract = True 42 | 43 | 44 | class ConcreteSon(AbstractBase): 45 | pass 46 | 47 | 48 | class AbstractSon(AbstractBase): 49 | class Meta: 50 | abstract = True 51 | 52 | 53 | class ConcreteGrandSon(AbstractSon): 54 | pass 55 | 56 | 57 | class StandardSon(StandardModel): 58 | pass 59 | 60 | 61 | class PointedModel(models.Model): 62 | foo = models.CharField(max_length=20) 63 | 64 | 65 | class PointerModel(models.Model): 66 | bar = models.CharField(max_length=20) 67 | pointed = models.OneToOneField( 68 | PointedModel, 69 | related_name='pointer', 70 | null=True, 71 | on_delete=models.CASCADE 72 | ) 73 | 74 | 75 | class WithDefaultValue(models.Model): 76 | foo = models.CharField(max_length=20, default='') 77 | 78 | 79 | class WithPassword(models.Model): 80 | pw = models.CharField(max_length=128) 81 | 82 | 83 | WITHFILE_UPLOAD_TO = 'django' 84 | WITHFILE_UPLOAD_DIR = os.path.join(settings.MEDIA_ROOT, WITHFILE_UPLOAD_TO) 85 | 86 | 87 | class WithFile(models.Model): 88 | afile = models.FileField(upload_to=WITHFILE_UPLOAD_TO) 89 | 90 | 91 | if Image is not None: # PIL is available 92 | 93 | class WithImage(models.Model): 94 | animage = models.ImageField(upload_to=WITHFILE_UPLOAD_TO) 95 | size = models.IntegerField(default=0) 96 | 97 | else: 98 | class WithImage(models.Model): 99 | pass 100 | 101 | 102 | class WithSignals(models.Model): 103 | foo = models.CharField(max_length=20) 104 | 105 | def __init__(self, post_save_signal_receiver=None): 106 | super().__init__() 107 | if post_save_signal_receiver: 108 | signals.post_save.connect( 109 | post_save_signal_receiver, 110 | sender=self.__class__, 111 | ) 112 | 113 | 114 | class CustomManager(models.Manager): 115 | 116 | def create(self, arg=None, **kwargs): 117 | return super().create(**kwargs) 118 | 119 | 120 | class WithCustomManager(models.Model): 121 | 122 | foo = models.CharField(max_length=20) 123 | 124 | objects = CustomManager() 125 | 126 | 127 | class AbstractWithCustomManager(models.Model): 128 | custom_objects = CustomManager() 129 | 130 | class Meta: 131 | abstract = True 132 | 133 | 134 | class FromAbstractWithCustomManager(AbstractWithCustomManager): 135 | pass 136 | 137 | 138 | class HasMultifieldModel(models.Model): 139 | multifield = models.ForeignKey(to=MultifieldModel, on_delete=models.CASCADE) 140 | -------------------------------------------------------------------------------- /tests/djapp/settings.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | """Settings for factory_boy/Django tests.""" 4 | 5 | import os 6 | 7 | FACTORY_ROOT = os.path.join( 8 | os.path.abspath(os.path.dirname(__file__)), # /path/to/fboy/tests/djapp/ 9 | os.pardir, # /path/to/fboy/tests/ 10 | os.pardir, # /path/to/fboy 11 | ) 12 | 13 | MEDIA_ROOT = os.path.join(FACTORY_ROOT, 'tmp_test') 14 | 15 | DATABASES = { 16 | 'default': { 17 | 'ENGINE': 'django.db.backends.sqlite3', 18 | }, 19 | 'replica': { 20 | 'ENGINE': 'django.db.backends.sqlite3', 21 | }, 22 | } 23 | 24 | 25 | INSTALLED_APPS = [ 26 | 'tests.djapp' 27 | ] 28 | 29 | MIDDLEWARE_CLASSES = () 30 | 31 | SECRET_KEY = 'testing.' 32 | 33 | # TODO: Will be the default after Django 5.0. Remove this setting when 34 | # Django 5.0 is the last supported version. 35 | USE_TZ = True 36 | -------------------------------------------------------------------------------- /tests/test_alchemy.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | """Tests for factory_boy/SQLAlchemy interactions.""" 4 | 5 | import unittest 6 | from unittest import mock 7 | 8 | try: 9 | import sqlalchemy 10 | except ImportError: 11 | raise unittest.SkipTest("sqlalchemy tests disabled.") 12 | 13 | import factory 14 | from factory.alchemy import SQLAlchemyModelFactory 15 | 16 | from .alchemyapp import models 17 | 18 | 19 | class StandardFactory(SQLAlchemyModelFactory): 20 | class Meta: 21 | model = models.StandardModel 22 | sqlalchemy_session = models.session 23 | 24 | id = factory.Sequence(lambda n: n) 25 | foo = factory.Sequence(lambda n: 'foo%d' % n) 26 | 27 | 28 | class NonIntegerPkFactory(SQLAlchemyModelFactory): 29 | class Meta: 30 | model = models.NonIntegerPk 31 | sqlalchemy_session = models.session 32 | 33 | id = factory.Sequence(lambda n: 'foo%d' % n) 34 | 35 | 36 | class NoSessionFactory(SQLAlchemyModelFactory): 37 | class Meta: 38 | model = models.StandardModel 39 | sqlalchemy_session = None 40 | 41 | id = factory.Sequence(lambda n: n) 42 | 43 | 44 | class MultifieldModelFactory(SQLAlchemyModelFactory): 45 | class Meta: 46 | model = models.MultiFieldModel 47 | sqlalchemy_get_or_create = ('slug',) 48 | sqlalchemy_session = models.session 49 | sqlalchemy_session_persistence = 'commit' 50 | 51 | id = factory.Sequence(lambda n: n) 52 | foo = factory.Sequence(lambda n: 'foo%d' % n) 53 | 54 | 55 | class WithGetOrCreateFieldFactory(SQLAlchemyModelFactory): 56 | class Meta: 57 | model = models.StandardModel 58 | sqlalchemy_get_or_create = ('foo',) 59 | sqlalchemy_session = models.session 60 | sqlalchemy_session_persistence = 'commit' 61 | 62 | id = factory.Sequence(lambda n: n) 63 | foo = factory.Sequence(lambda n: 'foo%d' % n) 64 | 65 | 66 | class WithMultipleGetOrCreateFieldsFactory(SQLAlchemyModelFactory): 67 | class Meta: 68 | model = models.MultifieldUniqueModel 69 | sqlalchemy_get_or_create = ("slug", "text",) 70 | sqlalchemy_session = models.session 71 | sqlalchemy_session_persistence = 'commit' 72 | 73 | id = factory.Sequence(lambda n: n) 74 | slug = factory.Sequence(lambda n: "slug%s" % n) 75 | text = factory.Sequence(lambda n: "text%s" % n) 76 | 77 | 78 | class TransactionTestCase(unittest.TestCase): 79 | def setUp(self): 80 | models.Base.metadata.create_all(models.engine) 81 | 82 | def tearDown(self): 83 | models.session.remove() 84 | models.Base.metadata.drop_all(models.engine) 85 | 86 | 87 | class SQLAlchemyPkSequenceTestCase(TransactionTestCase): 88 | def setUp(self): 89 | super().setUp() 90 | StandardFactory.reset_sequence(1) 91 | 92 | def test_pk_first(self): 93 | std = StandardFactory.build() 94 | self.assertEqual('foo1', std.foo) 95 | 96 | def test_pk_many(self): 97 | std1 = StandardFactory.build() 98 | std2 = StandardFactory.build() 99 | self.assertEqual('foo1', std1.foo) 100 | self.assertEqual('foo2', std2.foo) 101 | 102 | def test_pk_creation(self): 103 | std1 = StandardFactory.create() 104 | self.assertEqual('foo1', std1.foo) 105 | self.assertEqual(1, std1.id) 106 | 107 | StandardFactory.reset_sequence() 108 | std2 = StandardFactory.create() 109 | self.assertEqual('foo0', std2.foo) 110 | self.assertEqual(0, std2.id) 111 | 112 | def test_pk_force_value(self): 113 | std1 = StandardFactory.create(id=10) 114 | self.assertEqual('foo1', std1.foo) # sequence and pk are unrelated 115 | self.assertEqual(10, std1.id) 116 | 117 | StandardFactory.reset_sequence() 118 | std2 = StandardFactory.create() 119 | self.assertEqual('foo0', std2.foo) # Sequence doesn't care about pk 120 | self.assertEqual(0, std2.id) 121 | 122 | 123 | class SQLAlchemyGetOrCreateTests(TransactionTestCase): 124 | def test_simple_call(self): 125 | obj1 = WithGetOrCreateFieldFactory(foo='foo1') 126 | obj2 = WithGetOrCreateFieldFactory(foo='foo1') 127 | self.assertEqual(obj1, obj2) 128 | 129 | def test_missing_arg(self): 130 | with self.assertRaises(factory.FactoryError): 131 | MultifieldModelFactory() 132 | 133 | def test_raises_exception_when_existing_objs(self): 134 | StandardFactory.create_batch(2, foo='foo') 135 | with self.assertRaises(sqlalchemy.orm.exc.MultipleResultsFound): 136 | WithGetOrCreateFieldFactory(foo='foo') 137 | 138 | def test_multicall(self): 139 | objs = MultifieldModelFactory.create_batch( 140 | 6, 141 | slug=factory.Iterator(['main', 'alt']), 142 | ) 143 | self.assertEqual(6, len(objs)) 144 | self.assertEqual(2, len(set(objs))) 145 | self.assertEqual( 146 | list( 147 | obj.slug for obj in models.session.query( 148 | models.MultiFieldModel.slug 149 | ).order_by(models.MultiFieldModel.slug) 150 | ), 151 | ["alt", "main"], 152 | ) 153 | 154 | 155 | class MultipleGetOrCreateFieldsTest(TransactionTestCase): 156 | def test_one_defined(self): 157 | obj1 = WithMultipleGetOrCreateFieldsFactory() 158 | obj2 = WithMultipleGetOrCreateFieldsFactory(slug=obj1.slug) 159 | self.assertEqual(obj1, obj2) 160 | 161 | def test_both_defined(self): 162 | obj1 = WithMultipleGetOrCreateFieldsFactory() 163 | with self.assertRaises(sqlalchemy.exc.IntegrityError): 164 | WithMultipleGetOrCreateFieldsFactory(slug=obj1.slug, text="alt") 165 | 166 | def test_unique_field_not_in_get_or_create(self): 167 | WithMultipleGetOrCreateFieldsFactory(title='Title') 168 | with self.assertRaises(sqlalchemy.exc.IntegrityError): 169 | WithMultipleGetOrCreateFieldsFactory(title='Title') 170 | 171 | 172 | class SQLAlchemySessionPersistenceTestCase(unittest.TestCase): 173 | def setUp(self): 174 | super().setUp() 175 | self.mock_session = mock.NonCallableMagicMock(spec=models.session) 176 | 177 | def test_flushing(self): 178 | class FlushingPersistenceFactory(StandardFactory): 179 | class Meta: 180 | sqlalchemy_session = self.mock_session 181 | sqlalchemy_session_persistence = 'flush' 182 | 183 | self.mock_session.commit.assert_not_called() 184 | self.mock_session.flush.assert_not_called() 185 | 186 | FlushingPersistenceFactory.create() 187 | self.mock_session.commit.assert_not_called() 188 | self.mock_session.flush.assert_called_once_with() 189 | 190 | def test_committing(self): 191 | class CommittingPersistenceFactory(StandardFactory): 192 | class Meta: 193 | sqlalchemy_session = self.mock_session 194 | sqlalchemy_session_persistence = 'commit' 195 | 196 | self.mock_session.commit.assert_not_called() 197 | self.mock_session.flush.assert_not_called() 198 | 199 | CommittingPersistenceFactory.create() 200 | self.mock_session.commit.assert_called_once_with() 201 | self.mock_session.flush.assert_not_called() 202 | 203 | def test_noflush_nocommit(self): 204 | class InactivePersistenceFactory(StandardFactory): 205 | class Meta: 206 | sqlalchemy_session = self.mock_session 207 | sqlalchemy_session_persistence = None 208 | 209 | self.mock_session.commit.assert_not_called() 210 | self.mock_session.flush.assert_not_called() 211 | 212 | InactivePersistenceFactory.create() 213 | self.mock_session.commit.assert_not_called() 214 | self.mock_session.flush.assert_not_called() 215 | 216 | def test_type_error(self): 217 | with self.assertRaises(TypeError): 218 | class BadPersistenceFactory(StandardFactory): 219 | class Meta: 220 | sqlalchemy_session_persistence = 'invalid_persistence_option' 221 | model = models.StandardModel 222 | 223 | 224 | class SQLAlchemyNonIntegerPkTestCase(TransactionTestCase): 225 | def tearDown(self): 226 | super().tearDown() 227 | NonIntegerPkFactory.reset_sequence() 228 | 229 | def test_first(self): 230 | nonint = NonIntegerPkFactory.build() 231 | self.assertEqual('foo0', nonint.id) 232 | 233 | def test_many(self): 234 | nonint1 = NonIntegerPkFactory.build() 235 | nonint2 = NonIntegerPkFactory.build() 236 | 237 | self.assertEqual('foo0', nonint1.id) 238 | self.assertEqual('foo1', nonint2.id) 239 | 240 | def test_creation(self): 241 | nonint1 = NonIntegerPkFactory.create() 242 | self.assertEqual('foo0', nonint1.id) 243 | 244 | NonIntegerPkFactory.reset_sequence() 245 | nonint2 = NonIntegerPkFactory.build() 246 | self.assertEqual('foo0', nonint2.id) 247 | 248 | def test_force_pk(self): 249 | nonint1 = NonIntegerPkFactory.create(id='foo10') 250 | self.assertEqual('foo10', nonint1.id) 251 | 252 | NonIntegerPkFactory.reset_sequence() 253 | nonint2 = NonIntegerPkFactory.create() 254 | self.assertEqual('foo0', nonint2.id) 255 | 256 | 257 | class SQLAlchemyNoSessionTestCase(TransactionTestCase): 258 | 259 | def test_create_raises_exception_when_no_session_was_set(self): 260 | with self.assertRaises(RuntimeError): 261 | NoSessionFactory.create() 262 | 263 | def test_build_does_not_raises_exception_when_no_session_was_set(self): 264 | NoSessionFactory.reset_sequence() # Make sure we start at test ID 0 265 | inst0 = NoSessionFactory.build() 266 | inst1 = NoSessionFactory.build() 267 | self.assertEqual(inst0.id, 0) 268 | self.assertEqual(inst1.id, 1) 269 | 270 | 271 | class SQLAlchemySessionFactoryTestCase(TransactionTestCase): 272 | def test_create_get_session_from_sqlalchemy_session_factory(self): 273 | class SessionGetterFactory(SQLAlchemyModelFactory): 274 | class Meta: 275 | model = models.StandardModel 276 | sqlalchemy_session_factory = lambda: models.session 277 | 278 | id = factory.Sequence(lambda n: n) 279 | 280 | SessionGetterFactory.create() 281 | self.assertEqual(SessionGetterFactory._meta.sqlalchemy_session, models.session) 282 | # Reuse the session obtained from sqlalchemy_session_factory. 283 | SessionGetterFactory.create() 284 | 285 | def test_create_raise_exception_sqlalchemy_session_factory_not_callable(self): 286 | message = "^Provide either a sqlalchemy_session or a sqlalchemy_session_factory, not both$" 287 | with self.assertRaisesRegex(RuntimeError, message): 288 | class SessionAndGetterFactory(SQLAlchemyModelFactory): 289 | class Meta: 290 | model = models.StandardModel 291 | sqlalchemy_session = models.session 292 | sqlalchemy_session_factory = lambda: models.session 293 | 294 | id = factory.Sequence(lambda n: n) 295 | 296 | 297 | class NameConflictTests(TransactionTestCase): 298 | """Regression test for `TypeError: _save() got multiple values for argument 'session'` 299 | 300 | See #775. 301 | """ 302 | def test_no_name_conflict_on_save(self): 303 | class SpecialFieldWithSaveFactory(SQLAlchemyModelFactory): 304 | class Meta: 305 | model = models.SpecialFieldModel 306 | sqlalchemy_session = models.session 307 | 308 | id = factory.Sequence(lambda n: n) 309 | session = '' 310 | 311 | saved_child = SpecialFieldWithSaveFactory() 312 | self.assertEqual(saved_child.session, "") 313 | 314 | def test_no_name_conflict_on_get_or_create(self): 315 | class SpecialFieldWithGetOrCreateFactory(SQLAlchemyModelFactory): 316 | class Meta: 317 | model = models.SpecialFieldModel 318 | sqlalchemy_get_or_create = ('session',) 319 | sqlalchemy_session = models.session 320 | 321 | id = factory.Sequence(lambda n: n) 322 | session = '' 323 | 324 | get_or_created_child = SpecialFieldWithGetOrCreateFactory() 325 | self.assertEqual(get_or_created_child.session, "") 326 | -------------------------------------------------------------------------------- /tests/test_declarations.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | import datetime 4 | import unittest 5 | from unittest import mock 6 | 7 | from factory import base, declarations, errors, helpers 8 | 9 | from . import utils 10 | 11 | 12 | class OrderedDeclarationTestCase(unittest.TestCase): 13 | def test_errors(self): 14 | with self.assertRaises(NotImplementedError): 15 | utils.evaluate_declaration(declarations.OrderedDeclaration()) 16 | 17 | 18 | class DigTestCase(unittest.TestCase): 19 | class MyObj: 20 | def __init__(self, n): 21 | self.n = n 22 | 23 | def test_chaining(self): 24 | obj = self.MyObj(1) 25 | obj.a = self.MyObj(2) 26 | obj.a.b = self.MyObj(3) 27 | obj.a.b.c = self.MyObj(4) 28 | 29 | self.assertEqual(2, declarations.deepgetattr(obj, 'a').n) 30 | with self.assertRaises(AttributeError): 31 | declarations.deepgetattr(obj, 'b') 32 | self.assertEqual(2, declarations.deepgetattr(obj, 'a.n')) 33 | self.assertEqual(3, declarations.deepgetattr(obj, 'a.c', 3)) 34 | with self.assertRaises(AttributeError): 35 | declarations.deepgetattr(obj, 'a.c.n') 36 | with self.assertRaises(AttributeError): 37 | declarations.deepgetattr(obj, 'a.d') 38 | self.assertEqual(3, declarations.deepgetattr(obj, 'a.b').n) 39 | self.assertEqual(3, declarations.deepgetattr(obj, 'a.b.n')) 40 | self.assertEqual(4, declarations.deepgetattr(obj, 'a.b.c').n) 41 | self.assertEqual(4, declarations.deepgetattr(obj, 'a.b.c.n')) 42 | self.assertEqual(42, declarations.deepgetattr(obj, 'a.b.c.n.x', 42)) 43 | 44 | 45 | class MaybeTestCase(unittest.TestCase): 46 | def test_init(self): 47 | declarations.Maybe('foo', 1, 2) 48 | 49 | with self.assertRaisesRegex(TypeError, 'Inconsistent phases'): 50 | declarations.Maybe('foo', declarations.LazyAttribute(None), declarations.PostGenerationDeclaration()) 51 | 52 | 53 | class SelfAttributeTestCase(unittest.TestCase): 54 | def test_standard(self): 55 | a = declarations.SelfAttribute('foo.bar.baz') 56 | self.assertEqual(0, a.depth) 57 | self.assertEqual('foo.bar.baz', a.attribute_name) 58 | self.assertEqual(declarations._UNSPECIFIED, a.default) 59 | 60 | def test_dot(self): 61 | a = declarations.SelfAttribute('.bar.baz') 62 | self.assertEqual(1, a.depth) 63 | self.assertEqual('bar.baz', a.attribute_name) 64 | self.assertEqual(declarations._UNSPECIFIED, a.default) 65 | 66 | def test_default(self): 67 | a = declarations.SelfAttribute('bar.baz', 42) 68 | self.assertEqual(0, a.depth) 69 | self.assertEqual('bar.baz', a.attribute_name) 70 | self.assertEqual(42, a.default) 71 | 72 | def test_parent(self): 73 | a = declarations.SelfAttribute('..bar.baz') 74 | self.assertEqual(2, a.depth) 75 | self.assertEqual('bar.baz', a.attribute_name) 76 | self.assertEqual(declarations._UNSPECIFIED, a.default) 77 | 78 | def test_grandparent(self): 79 | a = declarations.SelfAttribute('...bar.baz') 80 | self.assertEqual(3, a.depth) 81 | self.assertEqual('bar.baz', a.attribute_name) 82 | self.assertEqual(declarations._UNSPECIFIED, a.default) 83 | 84 | 85 | class IteratorTestCase(unittest.TestCase): 86 | def test_cycle(self): 87 | it = declarations.Iterator([1, 2]) 88 | self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) 89 | self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) 90 | self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=2)) 91 | self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=3)) 92 | 93 | def test_no_cycling(self): 94 | it = declarations.Iterator([1, 2], cycle=False) 95 | self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) 96 | self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) 97 | with self.assertRaises(StopIteration): 98 | utils.evaluate_declaration(it, force_sequence=2) 99 | 100 | def test_initial_reset(self): 101 | it = declarations.Iterator([1, 2]) 102 | it.reset() 103 | 104 | def test_reset_cycle(self): 105 | it = declarations.Iterator([1, 2]) 106 | self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) 107 | self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) 108 | self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=2)) 109 | self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=3)) 110 | self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=4)) 111 | it.reset() 112 | self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=5)) 113 | self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=6)) 114 | 115 | def test_reset_no_cycling(self): 116 | it = declarations.Iterator([1, 2], cycle=False) 117 | self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) 118 | self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) 119 | with self.assertRaises(StopIteration): 120 | utils.evaluate_declaration(it, force_sequence=2) 121 | it.reset() 122 | self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) 123 | self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) 124 | with self.assertRaises(StopIteration): 125 | utils.evaluate_declaration(it, force_sequence=2) 126 | 127 | def test_getter(self): 128 | it = declarations.Iterator([(1, 2), (1, 3)], getter=lambda p: p[1]) 129 | self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=0)) 130 | self.assertEqual(3, utils.evaluate_declaration(it, force_sequence=1)) 131 | self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=2)) 132 | self.assertEqual(3, utils.evaluate_declaration(it, force_sequence=3)) 133 | 134 | 135 | class TransformerTestCase(unittest.TestCase): 136 | def test_transform(self): 137 | t = declarations.Transformer('foo', transform=str.upper) 138 | self.assertEqual("FOO", utils.evaluate_declaration(t)) 139 | 140 | 141 | class PostGenerationDeclarationTestCase(unittest.TestCase): 142 | def test_post_generation(self): 143 | call_params = [] 144 | 145 | def foo(*args, **kwargs): 146 | call_params.append(args) 147 | call_params.append(kwargs) 148 | 149 | helpers.build( 150 | dict, 151 | foo=declarations.PostGeneration(foo), 152 | foo__bar=42, 153 | blah=42, 154 | blah__baz=1, 155 | ) 156 | 157 | self.assertEqual(2, len(call_params)) 158 | self.assertEqual(3, len(call_params[0])) # instance, step, context.value 159 | self.assertEqual({'bar': 42}, call_params[1]) 160 | 161 | def test_decorator_simple(self): 162 | call_params = [] 163 | 164 | @helpers.post_generation 165 | def foo(*args, **kwargs): 166 | call_params.append(args) 167 | call_params.append(kwargs) 168 | 169 | helpers.build( 170 | dict, 171 | foo=foo, 172 | foo__bar=42, 173 | blah=42, 174 | blah__baz=1, 175 | ) 176 | 177 | self.assertEqual(2, len(call_params)) 178 | self.assertEqual(3, len(call_params[0])) # instance, step, context.value 179 | self.assertEqual({'bar': 42}, call_params[1]) 180 | 181 | 182 | class FactoryWrapperTestCase(unittest.TestCase): 183 | def test_invalid_path(self): 184 | with self.assertRaises(ValueError): 185 | declarations._FactoryWrapper('UnqualifiedSymbol') 186 | with self.assertRaises(ValueError): 187 | declarations._FactoryWrapper(42) 188 | 189 | def test_class(self): 190 | w = declarations._FactoryWrapper(datetime.date) 191 | self.assertEqual(datetime.date, w.get()) 192 | 193 | def test_path(self): 194 | w = declarations._FactoryWrapper('datetime.date') 195 | self.assertEqual(datetime.date, w.get()) 196 | 197 | def test_lazyness(self): 198 | f = declarations._FactoryWrapper('factory.declarations.Sequence') 199 | self.assertEqual(None, f.factory) 200 | 201 | factory_class = f.get() 202 | self.assertEqual(declarations.Sequence, factory_class) 203 | 204 | def test_cache(self): 205 | """Ensure that _FactoryWrapper tries to import only once.""" 206 | orig_date = datetime.date 207 | w = declarations._FactoryWrapper('datetime.date') 208 | self.assertEqual(None, w.factory) 209 | 210 | factory_class = w.get() 211 | self.assertEqual(orig_date, factory_class) 212 | 213 | try: 214 | # Modify original value 215 | datetime.date = None 216 | # Repeat import 217 | factory_class = w.get() 218 | self.assertEqual(orig_date, factory_class) 219 | 220 | finally: 221 | # IMPORTANT: restore attribute. 222 | datetime.date = orig_date 223 | 224 | 225 | class PostGenerationMethodCallTestCase(unittest.TestCase): 226 | def build(self, declaration, **params): 227 | f = helpers.make_factory(mock.MagicMock, post=declaration) 228 | return f(**params) 229 | 230 | def test_simplest_setup_and_call(self): 231 | obj = self.build( 232 | declarations.PostGenerationMethodCall('method'), 233 | ) 234 | obj.method.assert_called_once_with() 235 | 236 | def test_call_with_method_args(self): 237 | obj = self.build( 238 | declarations.PostGenerationMethodCall('method', 'data'), 239 | ) 240 | obj.method.assert_called_once_with('data') 241 | 242 | def test_call_with_passed_extracted_string(self): 243 | obj = self.build( 244 | declarations.PostGenerationMethodCall('method'), 245 | post='data', 246 | ) 247 | obj.method.assert_called_once_with('data') 248 | 249 | def test_call_with_passed_extracted_int(self): 250 | obj = self.build( 251 | declarations.PostGenerationMethodCall('method'), 252 | post=1, 253 | ) 254 | obj.method.assert_called_once_with(1) 255 | 256 | def test_call_with_passed_extracted_iterable(self): 257 | obj = self.build( 258 | declarations.PostGenerationMethodCall('method'), 259 | post=(1, 2, 3), 260 | ) 261 | obj.method.assert_called_once_with((1, 2, 3)) 262 | 263 | def test_call_with_method_kwargs(self): 264 | obj = self.build( 265 | declarations.PostGenerationMethodCall('method', data='data'), 266 | ) 267 | obj.method.assert_called_once_with(data='data') 268 | 269 | def test_call_with_passed_kwargs(self): 270 | obj = self.build( 271 | declarations.PostGenerationMethodCall('method'), 272 | post__data='other', 273 | ) 274 | obj.method.assert_called_once_with(data='other') 275 | 276 | def test_multi_call_with_multi_method_args(self): 277 | with self.assertRaises(errors.InvalidDeclarationError): 278 | self.build( 279 | declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), 280 | ) 281 | 282 | 283 | class PostGenerationOrdering(unittest.TestCase): 284 | 285 | def test_post_generation_declaration_order(self): 286 | postgen_results = [] 287 | 288 | class Related(base.Factory): 289 | class Meta: 290 | model = mock.MagicMock() 291 | 292 | class Ordered(base.Factory): 293 | class Meta: 294 | model = mock.MagicMock() 295 | 296 | a = declarations.RelatedFactory(Related) 297 | z = declarations.RelatedFactory(Related) 298 | 299 | @helpers.post_generation 300 | def a1(*args, **kwargs): 301 | postgen_results.append('a1') 302 | 303 | @helpers.post_generation 304 | def zz(*args, **kwargs): 305 | postgen_results.append('zz') 306 | 307 | @helpers.post_generation 308 | def aa(*args, **kwargs): 309 | postgen_results.append('aa') 310 | 311 | postgen_names = Ordered._meta.post_declarations.sorted() 312 | self.assertEqual(postgen_names, ['a', 'z', 'a1', 'zz', 'aa']) 313 | 314 | # Test generation happens in desired order 315 | Ordered() 316 | self.assertEqual(postgen_results, ['a1', 'zz', 'aa']) 317 | -------------------------------------------------------------------------------- /tests/test_dev_experience.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | """Tests about developer experience: help messages, errors, etc.""" 4 | 5 | import collections 6 | import unittest 7 | 8 | import factory 9 | import factory.errors 10 | 11 | Country = collections.namedtuple('Country', ['name', 'continent', 'capital_city']) 12 | City = collections.namedtuple('City', ['name', 'population']) 13 | 14 | 15 | class DeclarationTests(unittest.TestCase): 16 | def test_subfactory_to_model(self): 17 | """A helpful error message occurs when pointing a subfactory to a model.""" 18 | class CountryFactory(factory.Factory): 19 | class Meta: 20 | model = Country 21 | 22 | name = factory.Faker('country') 23 | continent = "Antarctica" 24 | 25 | # Error here: pointing the SubFactory to a model, not a factory. 26 | capital_city = factory.SubFactory(City) 27 | 28 | with self.assertRaises(factory.errors.AssociatedClassError) as raised: 29 | CountryFactory() 30 | 31 | self.assertIn('City', str(raised.exception)) 32 | self.assertIn('Country', str(raised.exception)) 33 | 34 | def test_subfactory_to_factorylike_model(self): 35 | """A helpful error message occurs when pointing a subfactory to a model. 36 | 37 | This time with a model that looks more like a factory (ie has a `._meta`).""" 38 | 39 | class CityModel: 40 | _meta = None 41 | name = "Coruscant" 42 | population = 0 43 | 44 | class CountryFactory(factory.Factory): 45 | class Meta: 46 | model = Country 47 | 48 | name = factory.Faker('country') 49 | continent = "Antarctica" 50 | 51 | # Error here: pointing the SubFactory to a model, not a factory. 52 | capital_city = factory.SubFactory(CityModel) 53 | 54 | with self.assertRaises(factory.errors.AssociatedClassError) as raised: 55 | CountryFactory() 56 | 57 | self.assertIn('CityModel', str(raised.exception)) 58 | self.assertIn('Country', str(raised.exception)) 59 | -------------------------------------------------------------------------------- /tests/test_docs_internals.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2015 Raphaël Barrois 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | """Tests for the docs/internals module.""" 21 | 22 | import datetime 23 | import unittest 24 | 25 | import factory 26 | import factory.fuzzy 27 | 28 | 29 | class User: 30 | def __init__( 31 | self, 32 | username, 33 | full_name, 34 | is_active=True, 35 | is_superuser=False, 36 | is_staff=False, 37 | creation_date=None, 38 | deactivation_date=None, 39 | ): 40 | self.username = username 41 | self.full_name = full_name 42 | self.is_active = is_active 43 | self.is_superuser = is_superuser 44 | self.is_staff = is_staff 45 | self.creation_date = creation_date 46 | self.deactivation_date = deactivation_date 47 | self.logs = [] 48 | 49 | def log(self, action, timestamp): 50 | UserLog(user=self, action=action, timestamp=timestamp) 51 | 52 | 53 | class UserLog: 54 | 55 | ACTIONS = ['create', 'update', 'disable'] 56 | 57 | def __init__(self, user, action, timestamp): 58 | self.user = user 59 | self.action = action 60 | self.timestamp = timestamp 61 | 62 | user.logs.append(self) 63 | 64 | 65 | class UserLogFactory(factory.Factory): 66 | class Meta: 67 | model = UserLog 68 | 69 | user = factory.SubFactory('test_docs_internals.UserFactory') 70 | timestamp = factory.fuzzy.FuzzyDateTime( 71 | datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc), 72 | ) 73 | action = factory.Iterator(UserLog.ACTIONS) 74 | 75 | 76 | class UserFactory(factory.Factory): 77 | class Meta: 78 | model = User 79 | 80 | class Params: 81 | # Allow us to quickly enable staff/superuser flags 82 | superuser = factory.Trait( 83 | is_superuser=True, 84 | is_staff=True, 85 | ) 86 | # Meta parameter handling all 'enabled'-related fields 87 | enabled = True 88 | 89 | # Classic fields 90 | username = factory.Faker('user_name') 91 | full_name = factory.Faker('name') 92 | creation_date = factory.fuzzy.FuzzyDateTime( 93 | datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc), 94 | datetime.datetime(2015, 12, 31, 20, tzinfo=datetime.timezone.utc) 95 | ) 96 | 97 | # Conditional flags 98 | is_active = factory.SelfAttribute('enabled') 99 | deactivation_date = factory.Maybe( 100 | 'enabled', 101 | None, 102 | factory.fuzzy.FuzzyDateTime( 103 | datetime.datetime.now().replace(tzinfo=datetime.timezone.utc) - datetime.timedelta(days=10), 104 | datetime.datetime.now().replace(tzinfo=datetime.timezone.utc) - datetime.timedelta(days=1), 105 | ), 106 | ) 107 | 108 | # Related logs 109 | creation_log = factory.RelatedFactory( 110 | UserLogFactory, 111 | factory_related_name='user', 112 | action='create', 113 | timestamp=factory.SelfAttribute('user.creation_date'), 114 | ) 115 | 116 | 117 | class DocsInternalsTests(unittest.TestCase): 118 | def test_simple_usage(self): 119 | user = UserFactory() 120 | 121 | # Default user should be active, not super 122 | self.assertTrue(user.is_active) 123 | self.assertFalse(user.is_superuser) 124 | self.assertFalse(user.is_staff) 125 | 126 | # We should have one log 127 | self.assertEqual(1, len(user.logs)) 128 | # And it should be a 'create' action linked to the user's creation_date 129 | self.assertEqual('create', user.logs[0].action) 130 | self.assertEqual(user, user.logs[0].user) 131 | self.assertEqual(user.creation_date, user.logs[0].timestamp) 132 | -------------------------------------------------------------------------------- /tests/test_faker.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | import collections 4 | import datetime 5 | import random 6 | import unittest 7 | 8 | import faker.providers 9 | 10 | import factory 11 | 12 | 13 | class MockFaker: 14 | def __init__(self, expected): 15 | self.expected = expected 16 | self.random = random.Random() 17 | 18 | def format(self, provider, **kwargs): 19 | return self.expected[provider] 20 | 21 | 22 | class AdvancedMockFaker: 23 | def __init__(self, handlers): 24 | self.handlers = handlers 25 | self.random = random.Random() 26 | 27 | def format(self, provider, **kwargs): 28 | handler = self.handlers[provider] 29 | return handler(**kwargs) 30 | 31 | 32 | class FakerTests(unittest.TestCase): 33 | def setUp(self): 34 | self._real_fakers = factory.Faker._FAKER_REGISTRY 35 | factory.Faker._FAKER_REGISTRY = {} 36 | 37 | def tearDown(self): 38 | factory.Faker._FAKER_REGISTRY = self._real_fakers 39 | 40 | def _setup_mock_faker(self, locale=None, **definitions): 41 | if locale is None: 42 | locale = factory.Faker._DEFAULT_LOCALE 43 | factory.Faker._FAKER_REGISTRY[locale] = MockFaker(definitions) 44 | 45 | def _setup_advanced_mock_faker(self, locale=None, **handlers): 46 | if locale is None: 47 | locale = factory.Faker._DEFAULT_LOCALE 48 | factory.Faker._FAKER_REGISTRY[locale] = AdvancedMockFaker(handlers) 49 | 50 | def test_simple_biased(self): 51 | self._setup_mock_faker(name="John Doe") 52 | faker_field = factory.Faker('name') 53 | self.assertEqual("John Doe", faker_field.evaluate(None, None, {'locale': None})) 54 | 55 | def test_full_factory(self): 56 | class Profile: 57 | def __init__(self, first_name, last_name, email): 58 | self.first_name = first_name 59 | self.last_name = last_name 60 | self.email = email 61 | 62 | class ProfileFactory(factory.Factory): 63 | class Meta: 64 | model = Profile 65 | first_name = factory.Faker('first_name') 66 | last_name = factory.Faker('last_name', locale='fr_FR') 67 | email = factory.Faker('email') 68 | 69 | self._setup_mock_faker(first_name="John", last_name="Doe", email="john.doe@example.org") 70 | self._setup_mock_faker(first_name="Jean", last_name="Valjean", email="jvaljean@exemple.fr", locale='fr_FR') 71 | 72 | profile = ProfileFactory() 73 | self.assertEqual("John", profile.first_name) 74 | self.assertEqual("Valjean", profile.last_name) 75 | self.assertEqual('john.doe@example.org', profile.email) 76 | 77 | def test_override_locale(self): 78 | class Profile: 79 | def __init__(self, first_name, last_name): 80 | self.first_name = first_name 81 | self.last_name = last_name 82 | 83 | class ProfileFactory(factory.Factory): 84 | class Meta: 85 | model = Profile 86 | 87 | first_name = factory.Faker('first_name') 88 | last_name = factory.Faker('last_name', locale='fr_FR') 89 | 90 | self._setup_mock_faker(first_name="John", last_name="Doe") 91 | self._setup_mock_faker(first_name="Jean", last_name="Valjean", locale='fr_FR') 92 | self._setup_mock_faker(first_name="Johannes", last_name="Brahms", locale='de_DE') 93 | 94 | profile = ProfileFactory() 95 | self.assertEqual("John", profile.first_name) 96 | self.assertEqual("Valjean", profile.last_name) 97 | 98 | with factory.Faker.override_default_locale('de_DE'): 99 | profile = ProfileFactory() 100 | self.assertEqual("Johannes", profile.first_name) 101 | self.assertEqual("Valjean", profile.last_name) 102 | 103 | profile = ProfileFactory() 104 | self.assertEqual("John", profile.first_name) 105 | self.assertEqual("Valjean", profile.last_name) 106 | 107 | def test_add_provider(self): 108 | class Face: 109 | def __init__(self, smiley, french_smiley): 110 | self.smiley = smiley 111 | self.french_smiley = french_smiley 112 | 113 | class FaceFactory(factory.Factory): 114 | class Meta: 115 | model = Face 116 | 117 | smiley = factory.Faker('smiley') 118 | french_smiley = factory.Faker('smiley', locale='fr_FR') 119 | 120 | class SmileyProvider(faker.providers.BaseProvider): 121 | def smiley(self): 122 | return ':)' 123 | 124 | class FrenchSmileyProvider(faker.providers.BaseProvider): 125 | def smiley(self): 126 | return '(:' 127 | 128 | factory.Faker.add_provider(SmileyProvider) 129 | factory.Faker.add_provider(FrenchSmileyProvider, 'fr_FR') 130 | 131 | face = FaceFactory() 132 | self.assertEqual(":)", face.smiley) 133 | self.assertEqual("(:", face.french_smiley) 134 | 135 | def test_faker_customization(self): 136 | """Factory declarations in Faker parameters should be accepted.""" 137 | Trip = collections.namedtuple('Trip', ['departure', 'transfer', 'arrival']) 138 | 139 | may_4th = datetime.date(1977, 5, 4) 140 | may_25th = datetime.date(1977, 5, 25) 141 | october_19th = datetime.date(1977, 10, 19) 142 | 143 | class TripFactory(factory.Factory): 144 | class Meta: 145 | model = Trip 146 | 147 | departure = may_4th 148 | arrival = may_25th 149 | transfer = factory.Faker( 150 | 'date_between_dates', 151 | start_date=factory.SelfAttribute('..departure'), 152 | end_date=factory.SelfAttribute('..arrival'), 153 | ) 154 | 155 | def fake_select_date(start_date, end_date): 156 | """Fake date_between_dates.""" 157 | # Ensure that dates have been transferred from the factory 158 | # to Faker parameters. 159 | self.assertEqual(start_date, may_4th) 160 | self.assertEqual(end_date, may_25th) 161 | return october_19th 162 | 163 | self._setup_advanced_mock_faker( 164 | date_between_dates=fake_select_date, 165 | ) 166 | 167 | trip = TripFactory() 168 | self.assertEqual(may_4th, trip.departure) 169 | self.assertEqual(october_19th, trip.transfer) 170 | self.assertEqual(may_25th, trip.arrival) 171 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | import io 4 | import logging 5 | import unittest 6 | 7 | from factory import helpers 8 | 9 | 10 | class DebugTest(unittest.TestCase): 11 | """Tests for the 'factory.debug()' helper.""" 12 | 13 | def test_default_logger(self): 14 | stream1 = io.StringIO() 15 | stream2 = io.StringIO() 16 | 17 | logger = logging.getLogger('factory.test') 18 | h = logging.StreamHandler(stream1) 19 | h.setLevel(logging.INFO) 20 | logger.addHandler(h) 21 | 22 | # Non-debug: no text gets out 23 | logger.debug("Test") 24 | self.assertEqual('', stream1.getvalue()) 25 | 26 | with helpers.debug(stream=stream2): 27 | # Debug: text goes to new stream only 28 | logger.debug("Test2") 29 | 30 | self.assertEqual('', stream1.getvalue()) 31 | self.assertEqual("Test2\n", stream2.getvalue()) 32 | 33 | def test_alternate_logger(self): 34 | stream1 = io.StringIO() 35 | stream2 = io.StringIO() 36 | 37 | l1 = logging.getLogger('factory.test') 38 | l2 = logging.getLogger('factory.foo') 39 | h = logging.StreamHandler(stream1) 40 | h.setLevel(logging.DEBUG) 41 | l2.addHandler(h) 42 | 43 | # Non-debug: no text gets out 44 | l1.debug("Test") 45 | self.assertEqual('', stream1.getvalue()) 46 | l2.debug("Test") 47 | self.assertEqual('', stream1.getvalue()) 48 | 49 | with helpers.debug('factory.test', stream=stream2): 50 | # Debug: text goes to new stream only 51 | l1.debug("Test2") 52 | l2.debug("Test3") 53 | 54 | self.assertEqual("", stream1.getvalue()) 55 | self.assertEqual("Test2\n", stream2.getvalue()) 56 | 57 | def test_restores_logging_on_error(self): 58 | class MyException(Exception): 59 | pass 60 | 61 | stream = io.StringIO() 62 | try: 63 | with helpers.debug(stream=stream): 64 | raise MyException 65 | except MyException: 66 | logger = logging.getLogger('factory') 67 | self.assertEqual(logger.level, logging.NOTSET) 68 | self.assertEqual(logger.handlers, []) 69 | -------------------------------------------------------------------------------- /tests/test_mongoengine.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | """Tests for factory_boy/MongoEngine interactions.""" 4 | 5 | import os 6 | import unittest 7 | 8 | try: 9 | import mongoengine 10 | except ImportError: 11 | raise unittest.SkipTest("mongodb tests disabled.") 12 | 13 | import mongomock 14 | 15 | import factory 16 | from factory.mongoengine import MongoEngineFactory 17 | 18 | 19 | class Address(mongoengine.EmbeddedDocument): 20 | street = mongoengine.StringField() 21 | 22 | 23 | class Person(mongoengine.Document): 24 | name = mongoengine.StringField() 25 | address = mongoengine.EmbeddedDocumentField(Address) 26 | 27 | 28 | class AddressFactory(MongoEngineFactory): 29 | class Meta: 30 | model = Address 31 | 32 | street = factory.Sequence(lambda n: 'street%d' % n) 33 | 34 | 35 | class PersonFactory(MongoEngineFactory): 36 | class Meta: 37 | model = Person 38 | 39 | name = factory.Sequence(lambda n: 'name%d' % n) 40 | address = factory.SubFactory(AddressFactory) 41 | 42 | 43 | class MongoEngineTestCase(unittest.TestCase): 44 | 45 | db_name = os.environ.get('MONGO_DATABASE', 'factory_boy_test') 46 | db_host = os.environ.get('MONGO_HOST', 'localhost') 47 | db_port = int(os.environ.get('MONGO_PORT', '27017')) 48 | server_timeout_ms = int(os.environ.get('MONGO_TIMEOUT', '300')) 49 | 50 | @classmethod 51 | def setUpClass(cls): 52 | from pymongo import read_preferences as mongo_rp 53 | cls.db = mongoengine.connect( 54 | db=cls.db_name, 55 | host=cls.db_host, 56 | port=cls.db_port, 57 | mongo_client_class=mongomock.MongoClient, 58 | # PyMongo>=2.1 requires an explicit read_preference. 59 | read_preference=mongo_rp.ReadPreference.PRIMARY, 60 | # PyMongo>=2.1 has a 20s timeout, use 100ms instead 61 | serverselectiontimeoutms=cls.server_timeout_ms, 62 | uuidRepresentation='standard', 63 | ) 64 | 65 | @classmethod 66 | def tearDownClass(cls): 67 | cls.db.drop_database(cls.db_name) 68 | 69 | def test_build(self): 70 | std = PersonFactory.build() 71 | self.assertEqual('name0', std.name) 72 | self.assertEqual('street0', std.address.street) 73 | self.assertIsNone(std.id) 74 | 75 | def test_creation(self): 76 | std1 = PersonFactory.create() 77 | self.assertEqual('name1', std1.name) 78 | self.assertEqual('street1', std1.address.street) 79 | self.assertIsNotNone(std1.id) 80 | -------------------------------------------------------------------------------- /tests/test_regression.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | """Regression tests related to issues found with the project""" 5 | 6 | import datetime 7 | import typing as T 8 | import unittest 9 | 10 | import factory 11 | 12 | # Example objects 13 | # =============== 14 | 15 | 16 | class Author(T.NamedTuple): 17 | fullname: str 18 | pseudonym: T.Optional[str] = None 19 | 20 | 21 | class Book(T.NamedTuple): 22 | title: str 23 | author: Author 24 | 25 | 26 | class PublishedBook(T.NamedTuple): 27 | book: Book 28 | published_on: datetime.date 29 | countries: T.List[str] 30 | 31 | 32 | class FakerRegressionTests(unittest.TestCase): 33 | def test_locale_issue(self): 34 | """Regression test for `KeyError: 'locale'` 35 | 36 | See #785 #786 #787 #788 #790 #796. 37 | """ 38 | class AuthorFactory(factory.Factory): 39 | class Meta: 40 | model = Author 41 | 42 | class Params: 43 | unknown = factory.Trait( 44 | fullname="", 45 | ) 46 | 47 | fullname = factory.Faker("name") 48 | 49 | public_author = AuthorFactory(unknown=False) 50 | self.assertIsNone(public_author.pseudonym) 51 | 52 | unknown_author = AuthorFactory(unknown=True) 53 | self.assertEqual("", unknown_author.fullname) 54 | 55 | def test_evaluated_without_locale(self): 56 | """Regression test for `KeyError: 'locale'` raised in `evaluate`. 57 | 58 | See #965 59 | 60 | """ 61 | class AuthorFactory(factory.Factory): 62 | fullname = factory.Faker("name") 63 | pseudonym = factory.Maybe( 64 | decider=factory.Faker("pybool"), 65 | yes_declaration="yes", 66 | no_declaration="no", 67 | ) 68 | 69 | class Meta: 70 | model = Author 71 | 72 | author = AuthorFactory() 73 | 74 | self.assertIn(author.pseudonym, ["yes", "no"]) 75 | -------------------------------------------------------------------------------- /tests/test_transformer.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | from unittest import TestCase 4 | 5 | import factory 6 | 7 | 8 | class TransformCounter: 9 | calls_count = 0 10 | 11 | @classmethod 12 | def __call__(cls, x): 13 | cls.calls_count += 1 14 | return x.upper() 15 | 16 | @classmethod 17 | def reset(cls): 18 | cls.calls_count = 0 19 | 20 | 21 | transform = TransformCounter() 22 | 23 | 24 | class Upper: 25 | def __init__(self, name, **extra): 26 | self.name = name 27 | self.extra = extra 28 | 29 | 30 | class UpperFactory(factory.Factory): 31 | name = factory.Transformer("value", transform=transform) 32 | 33 | class Meta: 34 | model = Upper 35 | 36 | 37 | class TransformerTest(TestCase): 38 | def setUp(self): 39 | transform.reset() 40 | 41 | def test_transform_count(self): 42 | self.assertEqual("VALUE", UpperFactory().name) 43 | self.assertEqual(transform.calls_count, 1) 44 | 45 | def test_transform_kwarg(self): 46 | self.assertEqual("TEST", UpperFactory(name="test").name) 47 | self.assertEqual(transform.calls_count, 1) 48 | self.assertEqual("VALUE", UpperFactory().name) 49 | self.assertEqual(transform.calls_count, 2) 50 | 51 | def test_transform_faker(self): 52 | value = UpperFactory(name=factory.Faker("first_name_female", locale="fr")).name 53 | self.assertIs(value.isupper(), True) 54 | 55 | def test_transform_linked(self): 56 | value = UpperFactory( 57 | name=factory.LazyAttribute(lambda o: o.username.replace(".", " ")), 58 | username="john.doe", 59 | ).name 60 | self.assertEqual(value, "JOHN DOE") 61 | 62 | def test_force_value(self): 63 | value = UpperFactory(name=factory.Transformer.Force("Mia")).name 64 | self.assertEqual(value, "Mia") 65 | 66 | def test_force_value_declaration(self): 67 | """Pretty unlikely use case, but easy enough to cover.""" 68 | value = UpperFactory( 69 | name=factory.Transformer.Force( 70 | factory.LazyFunction(lambda: "infinity") 71 | ) 72 | ).name 73 | self.assertEqual(value, "infinity") 74 | 75 | def test_force_value_declaration_context(self): 76 | """Ensure "forced" values run at the right level.""" 77 | value = UpperFactory( 78 | name=factory.Transformer.Force( 79 | factory.LazyAttribute(lambda o: o.username.replace(".", " ")), 80 | ), 81 | username="john.doe", 82 | ).name 83 | self.assertEqual(value, "john doe") 84 | 85 | 86 | class TestObject: 87 | def __init__(self, one=None, two=None, three=None): 88 | self.one = one 89 | self.two = two 90 | self.three = three 91 | 92 | 93 | class TransformDeclarationFactory(factory.Factory): 94 | class Meta: 95 | model = TestObject 96 | one = factory.Transformer("", transform=str.upper) 97 | two = factory.Transformer(factory.Sequence(int), transform=lambda n: n ** 2) 98 | 99 | 100 | class TransformerSequenceTest(TestCase): 101 | def test_on_sequence(self): 102 | instance = TransformDeclarationFactory(__sequence=2) 103 | self.assertEqual(instance.one, "") 104 | self.assertEqual(instance.two, 4) 105 | self.assertIsNone(instance.three) 106 | 107 | def test_on_user_supplied(self): 108 | """A transformer can wrap a call-time declaration""" 109 | instance = TransformDeclarationFactory( 110 | one=factory.Sequence(str), 111 | two=2, 112 | __sequence=2, 113 | ) 114 | self.assertEqual(instance.one, "2") 115 | self.assertEqual(instance.two, 4) 116 | self.assertIsNone(instance.three) 117 | 118 | 119 | class WithMaybeFactory(factory.Factory): 120 | class Meta: 121 | model = TestObject 122 | 123 | one = True 124 | two = factory.Maybe( 125 | 'one', 126 | yes_declaration=factory.Transformer("yes", transform=str.upper), 127 | no_declaration=factory.Transformer("no", transform=str.upper), 128 | ) 129 | three = factory.Maybe('one', no_declaration=factory.Transformer("three", transform=str.upper)) 130 | 131 | 132 | class TransformerMaybeTest(TestCase): 133 | def test_default_transform(self): 134 | instance = WithMaybeFactory() 135 | self.assertIs(instance.one, True) 136 | self.assertEqual(instance.two, "YES") 137 | self.assertIsNone(instance.three) 138 | 139 | def test_yes_transform(self): 140 | instance = WithMaybeFactory(one=True) 141 | self.assertIs(instance.one, True) 142 | self.assertEqual(instance.two, "YES") 143 | self.assertIsNone(instance.three) 144 | 145 | def test_no_transform(self): 146 | instance = WithMaybeFactory(one=False) 147 | self.assertIs(instance.one, False) 148 | self.assertEqual(instance.two, "NO") 149 | self.assertEqual(instance.three, "THREE") 150 | 151 | def test_override(self): 152 | instance = WithMaybeFactory(one=True, two="NI") 153 | self.assertIs(instance.one, True) 154 | self.assertEqual(instance.two, "NI") 155 | self.assertIsNone(instance.three) 156 | 157 | 158 | class RelatedTest(TestCase): 159 | def test_default_transform(self): 160 | cities = [] 161 | 162 | class City: 163 | def __init__(self, capital_of, name): 164 | self.capital_of = capital_of 165 | self.name = name 166 | cities.append(self) 167 | 168 | class Country: 169 | def __init__(self, name): 170 | self.name = name 171 | 172 | class CityFactory(factory.Factory): 173 | class Meta: 174 | model = City 175 | 176 | name = "Rennes" 177 | 178 | class CountryFactory(factory.Factory): 179 | class Meta: 180 | model = Country 181 | 182 | name = "France" 183 | capital_city = factory.RelatedFactory( 184 | CityFactory, 185 | factory_related_name="capital_of", 186 | name=factory.Transformer("Paris", transform=str.upper), 187 | ) 188 | 189 | instance = CountryFactory() 190 | self.assertEqual(instance.name, "France") 191 | [city] = cities 192 | self.assertEqual(city.capital_of, instance) 193 | self.assertEqual(city.name, "PARIS") 194 | 195 | 196 | class WithTraitFactory(factory.Factory): 197 | class Meta: 198 | model = TestObject 199 | 200 | class Params: 201 | upper_two = factory.Trait( 202 | two=factory.Transformer("two", transform=str.upper) 203 | ) 204 | odds = factory.Trait( 205 | one="one", 206 | three="three", 207 | ) 208 | one = factory.Transformer("one", transform=str.upper) 209 | 210 | 211 | class TransformerTraitTest(TestCase): 212 | def test_traits_off(self): 213 | instance = WithTraitFactory() 214 | self.assertEqual(instance.one, "ONE") 215 | self.assertIsNone(instance.two) 216 | self.assertIsNone(instance.three) 217 | 218 | def test_trait_transform_applies(self): 219 | """A trait-provided transformer should apply to existing values""" 220 | instance = WithTraitFactory(upper_two=True) 221 | self.assertEqual(instance.one, "ONE") 222 | self.assertEqual(instance.two, "TWO") 223 | self.assertIsNone(instance.three) 224 | 225 | def test_trait_transform_applies_supplied(self): 226 | """A trait-provided transformer should be overridden by caller-provided values""" 227 | instance = WithTraitFactory(upper_two=True, two="two") 228 | self.assertEqual(instance.one, "ONE") 229 | self.assertEqual(instance.two, "two") 230 | self.assertIsNone(instance.three) 231 | -------------------------------------------------------------------------------- /tests/test_typing.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | import dataclasses 4 | import unittest 5 | 6 | import factory 7 | 8 | 9 | @dataclasses.dataclass 10 | class User: 11 | name: str 12 | email: str 13 | id: int 14 | 15 | 16 | class TypingTests(unittest.TestCase): 17 | 18 | def test_simple_factory(self) -> None: 19 | 20 | class UserFactory(factory.Factory[User]): 21 | name = "John Doe" 22 | email = "john.doe@example.org" 23 | id = 42 24 | 25 | class Meta: 26 | model = User 27 | 28 | result: User 29 | result = UserFactory.build() 30 | result = UserFactory.create() 31 | self.assertEqual(result.name, "John Doe") 32 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | import itertools 5 | import unittest 6 | 7 | from factory import utils 8 | 9 | 10 | class ImportObjectTestCase(unittest.TestCase): 11 | def test_datetime(self): 12 | imported = utils.import_object('datetime', 'date') 13 | import datetime 14 | d = datetime.date 15 | self.assertEqual(d, imported) 16 | 17 | def test_unknown_attribute(self): 18 | with self.assertRaises(AttributeError): 19 | utils.import_object('datetime', 'foo') 20 | 21 | def test_invalid_module(self): 22 | with self.assertRaises(ImportError): 23 | utils.import_object('this-is-an-invalid-module', '__name__') 24 | 25 | 26 | class LogPPrintTestCase(unittest.TestCase): 27 | def test_nothing(self): 28 | txt = str(utils.log_pprint()) 29 | self.assertEqual('', txt) 30 | 31 | def test_only_args(self): 32 | txt = str(utils.log_pprint((1, 2, 3))) 33 | self.assertEqual('1, 2, 3', txt) 34 | 35 | def test_only_kwargs(self): 36 | txt = str(utils.log_pprint(kwargs={'a': 1, 'b': 2})) 37 | self.assertIn(txt, ['a=1, b=2', 'b=2, a=1']) 38 | 39 | def test_bytes_args(self): 40 | txt = str(utils.log_pprint((b'\xe1\xe2',))) 41 | expected = "b'\\xe1\\xe2'" 42 | self.assertEqual(expected, txt) 43 | 44 | def test_text_args(self): 45 | txt = str(utils.log_pprint(('ŧêßŧ',))) 46 | expected = "'ŧêßŧ'" 47 | self.assertEqual(expected, txt) 48 | 49 | def test_bytes_kwargs(self): 50 | txt = str(utils.log_pprint(kwargs={'x': b'\xe1\xe2', 'y': b'\xe2\xe1'})) 51 | expected1 = "x=b'\\xe1\\xe2', y=b'\\xe2\\xe1'" 52 | expected2 = "y=b'\\xe2\\xe1', x=b'\\xe1\\xe2'" 53 | self.assertIn(txt, (expected1, expected2)) 54 | 55 | def test_text_kwargs(self): 56 | txt = str(utils.log_pprint(kwargs={'x': 'ŧêßŧ', 'y': 'ŧßêŧ'})) 57 | expected1 = "x='ŧêßŧ', y='ŧßêŧ'" 58 | expected2 = "y='ŧßêŧ', x='ŧêßŧ'" 59 | self.assertIn(txt, (expected1, expected2)) 60 | 61 | 62 | class ResetableIteratorTestCase(unittest.TestCase): 63 | def test_no_reset(self): 64 | i = utils.ResetableIterator([1, 2, 3]) 65 | self.assertEqual([1, 2, 3], list(i)) 66 | 67 | def test_no_reset_new_iterator(self): 68 | i = utils.ResetableIterator([1, 2, 3]) 69 | iterator = iter(i) 70 | self.assertEqual(1, next(iterator)) 71 | self.assertEqual(2, next(iterator)) 72 | 73 | iterator2 = iter(i) 74 | self.assertEqual(3, next(iterator2)) 75 | 76 | def test_infinite(self): 77 | i = utils.ResetableIterator(itertools.cycle([1, 2, 3])) 78 | iterator = iter(i) 79 | values = [next(iterator) for _i in range(10)] 80 | self.assertEqual([1, 2, 3, 1, 2, 3, 1, 2, 3, 1], values) 81 | 82 | def test_reset_simple(self): 83 | i = utils.ResetableIterator([1, 2, 3]) 84 | iterator = iter(i) 85 | self.assertEqual(1, next(iterator)) 86 | self.assertEqual(2, next(iterator)) 87 | 88 | i.reset() 89 | self.assertEqual(1, next(iterator)) 90 | self.assertEqual(2, next(iterator)) 91 | self.assertEqual(3, next(iterator)) 92 | 93 | def test_reset_at_begin(self): 94 | i = utils.ResetableIterator([1, 2, 3]) 95 | iterator = iter(i) 96 | i.reset() 97 | i.reset() 98 | self.assertEqual(1, next(iterator)) 99 | self.assertEqual(2, next(iterator)) 100 | self.assertEqual(3, next(iterator)) 101 | 102 | def test_reset_at_end(self): 103 | i = utils.ResetableIterator([1, 2, 3]) 104 | iterator = iter(i) 105 | self.assertEqual(1, next(iterator)) 106 | self.assertEqual(2, next(iterator)) 107 | self.assertEqual(3, next(iterator)) 108 | 109 | i.reset() 110 | self.assertEqual(1, next(iterator)) 111 | self.assertEqual(2, next(iterator)) 112 | self.assertEqual(3, next(iterator)) 113 | 114 | def test_reset_after_end(self): 115 | i = utils.ResetableIterator([1, 2, 3]) 116 | iterator = iter(i) 117 | self.assertEqual(1, next(iterator)) 118 | self.assertEqual(2, next(iterator)) 119 | self.assertEqual(3, next(iterator)) 120 | with self.assertRaises(StopIteration): 121 | next(iterator) 122 | 123 | i.reset() 124 | # Previous iter() has stopped 125 | iterator = iter(i) 126 | self.assertEqual(1, next(iterator)) 127 | self.assertEqual(2, next(iterator)) 128 | self.assertEqual(3, next(iterator)) 129 | 130 | def test_reset_twice(self): 131 | i = utils.ResetableIterator([1, 2, 3, 4, 5]) 132 | iterator = iter(i) 133 | self.assertEqual(1, next(iterator)) 134 | self.assertEqual(2, next(iterator)) 135 | 136 | i.reset() 137 | self.assertEqual(1, next(iterator)) 138 | self.assertEqual(2, next(iterator)) 139 | self.assertEqual(3, next(iterator)) 140 | self.assertEqual(4, next(iterator)) 141 | 142 | i.reset() 143 | self.assertEqual(1, next(iterator)) 144 | self.assertEqual(2, next(iterator)) 145 | self.assertEqual(3, next(iterator)) 146 | self.assertEqual(4, next(iterator)) 147 | 148 | def test_reset_shorter(self): 149 | i = utils.ResetableIterator([1, 2, 3, 4, 5]) 150 | iterator = iter(i) 151 | self.assertEqual(1, next(iterator)) 152 | self.assertEqual(2, next(iterator)) 153 | self.assertEqual(3, next(iterator)) 154 | self.assertEqual(4, next(iterator)) 155 | 156 | i.reset() 157 | self.assertEqual(1, next(iterator)) 158 | self.assertEqual(2, next(iterator)) 159 | 160 | i.reset() 161 | self.assertEqual(1, next(iterator)) 162 | self.assertEqual(2, next(iterator)) 163 | self.assertEqual(3, next(iterator)) 164 | self.assertEqual(4, next(iterator)) 165 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | import pathlib 4 | import unittest 5 | 6 | import factory 7 | 8 | SETUP_CFG_VERSION_PREFIX = "version =" 9 | 10 | 11 | class VersionTestCase(unittest.TestCase): 12 | def get_setupcfg_version(self): 13 | setup_cfg_path = pathlib.Path(__file__).parent.parent / "setup.cfg" 14 | with setup_cfg_path.open("r") as f: 15 | for line in f: 16 | if line.startswith(SETUP_CFG_VERSION_PREFIX): 17 | return line[len(SETUP_CFG_VERSION_PREFIX):].strip() 18 | 19 | def test_version(self): 20 | self.assertEqual(factory.__version__, self.get_setupcfg_version()) 21 | -------------------------------------------------------------------------------- /tests/testdata/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | 4 | import os.path 5 | 6 | TESTDATA_ROOT = os.path.abspath(os.path.dirname(__file__)) 7 | TESTFILE_PATH = os.path.join(TESTDATA_ROOT, 'example.data') 8 | TESTIMAGE_PATH = os.path.join(TESTDATA_ROOT, 'example.jpeg') 9 | -------------------------------------------------------------------------------- /tests/testdata/example.data: -------------------------------------------------------------------------------- 1 | example_data 2 | -------------------------------------------------------------------------------- /tests/testdata/example.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FactoryBoy/factory_boy/68feb45e182f9acccfde671b7ba0babb5bc7ce11/tests/testdata/example.jpeg -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright: See the LICENSE file. 2 | 3 | import functools 4 | import warnings 5 | 6 | import factory 7 | 8 | from . import alter_time 9 | 10 | 11 | def disable_warnings(fun): 12 | @functools.wraps(fun) 13 | def decorated(*args, **kwargs): 14 | with warnings.catch_warnings(): 15 | warnings.simplefilter('ignore') 16 | return fun(*args, **kwargs) 17 | return decorated 18 | 19 | 20 | class MultiModulePatcher: 21 | """An abstract context processor for patching multiple modules.""" 22 | 23 | def __init__(self, *target_modules, **kwargs): 24 | super().__init__(**kwargs) 25 | self.patchers = [self._build_patcher(mod) for mod in target_modules] 26 | 27 | def _build_patcher(self, target_module): # pragma: no cover 28 | """Build a mock patcher for the target module.""" 29 | raise NotImplementedError() 30 | 31 | def __enter__(self): 32 | for patcher in self.patchers: 33 | patcher.start() 34 | 35 | def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): 36 | for patcher in self.patchers: 37 | patcher.stop() 38 | 39 | 40 | class mocked_date_today(MultiModulePatcher): 41 | """A context processor changing the value of date.today().""" 42 | 43 | def __init__(self, target_date, *target_modules, **kwargs): 44 | self.target_date = target_date 45 | super().__init__(*target_modules, **kwargs) 46 | 47 | def _build_patcher(self, target_module): 48 | module_datetime = getattr(target_module, 'datetime') 49 | return alter_time.mock_date_today(self.target_date, module_datetime) 50 | 51 | 52 | class mocked_datetime_now(MultiModulePatcher): 53 | def __init__(self, target_dt, *target_modules, **kwargs): 54 | self.target_dt = target_dt 55 | super().__init__(*target_modules, **kwargs) 56 | 57 | def _build_patcher(self, target_module): 58 | module_datetime = getattr(target_module, 'datetime') 59 | return alter_time.mock_datetime_now(self.target_dt, module_datetime) 60 | 61 | 62 | def evaluate_declaration(declaration, force_sequence=None): 63 | kwargs = {'attr': declaration} 64 | if force_sequence is not None: 65 | kwargs['__sequence'] = force_sequence 66 | 67 | return factory.build(dict, **kwargs)['attr'] 68 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 1.9 3 | envlist = 4 | lint 5 | docs 6 | examples 7 | linkcheck 8 | py{39,310,311,312,313,py39,py310} 9 | py{39,310,311,312,313}-django42-mongo-alchemy 10 | py{py39,py310}-django42-mongo-alchemy 11 | py{310,311,312,313}-django51-mongo-alchemy 12 | pypy310-django51-mongo-alchemy 13 | py310-djangomain-mongo-alchemy 14 | 15 | [gh-actions] 16 | python = 17 | 3.9: py39 18 | 3.10: py310 19 | 3.11: py311 20 | 3.12: py312 21 | 3.13: py313 22 | pypy-3.10: pypy310 23 | 24 | [testenv] 25 | deps = 26 | mypy 27 | alchemy: SQLAlchemy 28 | mongo: mongoengine 29 | mongo: mongomock 30 | # mongomock imports pkg_resources, provided by setuptools. 31 | mongo: setuptools>=66.1.1 32 | django{42,51,main}: Pillow 33 | django42: Django>=4.2,<5.0 34 | django51: Django>=5.1,<5.2 35 | djangomain: Django>5.1,<6.0 36 | 37 | setenv = 38 | py: DJANGO_SETTINGS_MODULE=tests.djapp.settings 39 | 40 | pip_pre = 41 | djangomain: true 42 | 43 | allowlist_externals = make 44 | commands = make test 45 | 46 | [testenv:docs] 47 | extras = doc 48 | 49 | whitelist_externals = make 50 | commands = make doc spelling 51 | 52 | [testenv:examples] 53 | deps = 54 | -rexamples/requirements.txt 55 | 56 | whitelist_externals = make 57 | commands = make example-test 58 | 59 | [testenv:linkcheck] 60 | extras = doc 61 | 62 | whitelist_externals = make 63 | commands = make linkcheck 64 | 65 | [testenv:lint] 66 | deps = 67 | -rexamples/requirements.txt 68 | check_manifest 69 | extras = dev 70 | 71 | whitelist_externals = make 72 | commands = make lint 73 | --------------------------------------------------------------------------------