From c11bb0f0935dd01682f4e30f5d46b0f7d6d90917 Mon Sep 17 00:00:00 2001 From: Rust Saiargaliev Date: Fri, 27 Oct 2023 16:38:43 +0200 Subject: [PATCH] Fix #47 -- migrate docs to Markdown via MyST --- .readthedocs.yaml | 3 - docs/Makefile | 177 --------- docs/basic_usage.md | 321 +++++++++++++++ docs/{source => }/conf.py | 12 +- docs/how_bakery_behaves.md | 126 ++++++ docs/index.md | 45 +++ docs/make.bat | 242 ------------ docs/migrating_from_mommy.md | 16 + docs/recipes.md | 361 +++++++++++++++++ docs/source/basic_usage.rst | 306 --------------- docs/source/how_bakery_behaves.rst | 137 ------- docs/source/index.rst | 53 --- docs/source/migrating_from_mommy.rst | 18 - docs/source/recipes.rst | 365 ------------------ .../test_runners.rst => test_runners.md} | 19 +- pyproject.toml | 1 + 16 files changed, 889 insertions(+), 1313 deletions(-) delete mode 100644 docs/Makefile create mode 100644 docs/basic_usage.md rename docs/{source => }/conf.py (66%) create mode 100644 docs/how_bakery_behaves.md create mode 100644 docs/index.md delete mode 100644 docs/make.bat create mode 100644 docs/migrating_from_mommy.md create mode 100644 docs/recipes.md delete mode 100644 docs/source/basic_usage.rst delete mode 100644 docs/source/how_bakery_behaves.rst delete mode 100644 docs/source/index.rst delete mode 100644 docs/source/migrating_from_mommy.rst delete mode 100644 docs/source/recipes.rst rename docs/{source/test_runners.rst => test_runners.md} (67%) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 12cd4178..98d9e442 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,6 +11,3 @@ python: path: . extra_requirements: - docs - -sphinx: - configuration: docs/source/conf.py diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 201d55a4..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ModelMommy.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ModelMommy.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/ModelMommy" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ModelMommy" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/basic_usage.md b/docs/basic_usage.md new file mode 100644 index 00000000..ae342552 --- /dev/null +++ b/docs/basic_usage.md @@ -0,0 +1,321 @@ +# Basic Usage + +Let's say you have an app **shop** with a model like this: + +File: **models.py** + +```python +class Customer(models.Model): + """ + Model class Customer of shop app + """ + enjoy_jards_macale = models.BooleanField() + name = models.CharField(max_length=30) + email = models.EmailField() + age = models.IntegerField() + bio = models.TextField() + days_since_last_login = models.BigIntegerField() + birthday = models.DateField() + last_shopping = models.DateTimeField() +``` + +To create a persisted instance, just call Model Bakery: + +File: **test_models.py** + +```python +#Core Django imports +from django.test import TestCase + +#Third-party app imports +from model_bakery import baker + +from shop.models import Customer + +class CustomerTestModel(TestCase): + """ + Class to test the model Customer + """ + + def setUp(self): + self.customer = baker.make(Customer) +``` + +Importing every model over and over again is boring. So let Model Bakery import them for you: + +```python +from model_bakery import baker + +# 1st form: app_label.model_name +customer = baker.make('shop.Customer') + +# 2nd form: model_name +product = baker.make('Product') +``` + +```{note} +You can only use the 2nd form on unique model names. If you have an app shop with a Product, and an app stock with a Product, you must use the app_label.model_name form. +``` + +```{note} +model_name is case insensitive. +``` + +## Model Relationships + +Model Bakery also handles relationships. Let's say the customer has a purchase history: + +File: **models.py** + +```python +class Customer(models.Model): + """ + Model class Customer of shop app + """ + enjoy_jards_macale = models.BooleanField() + name = models.CharField(max_length=30) + email = models.EmailField() + age = models.IntegerField() + bio = models.TextField() + days_since_last_login = models.BigIntegerField() + birthday = models.DateField() + appointment = models.DateTimeField() + +class PurchaseHistory(models.Model): + """ + Model class PurchaseHistory of shop app + """ + customer = models.ForeignKey('Customer') + products = models.ManyToManyField('Product') + year = models.IntegerField() +``` + +You can use Model Bakery as: + +```python +from django.test import TestCase + +from model_bakery import baker + +class PurchaseHistoryTestModel(TestCase): + + def setUp(self): + self.history = baker.make('shop.PurchaseHistory') + print(self.history.customer) +``` + +It will also create the Customer, automagically. + +**NOTE: ForeignKeys and OneToOneFields** - Since Django 1.8, ForeignKey and OneToOne fields don't accept unpersisted model instances anymore. This means that if you run: + +```python +baker.prepare('shop.PurchaseHistory') +``` + +You'll end up with a persisted "Customer" instance. + +## M2M Relationships + +By default, Model Bakery doesn't create related instances for many-to-many relationships. +If you want them to be created, you have to turn it on as the following: + +```python +from django.test import TestCase + +from model_bakery import baker + +class PurchaseHistoryTestModel(TestCase): + + def setUp(self): + self.history = baker.make('shop.PurchaseHistory', make_m2m=True) + print(self.history.products.count()) +``` + +## Explicit M2M Relationships + +If you want to, you can prepare your own set of related object and pass it to Model Bakery. Here's an example: + +```python +products_set = baker.prepare(Product, _quantity=5) +history = baker.make(PurchaseHistory, products=products_set) +``` + +## Explicit values for fields + +By default, Model Bakery uses random values to populate the model's fields. But it's possible to explicitly set values for them as well. + +```python +from django.test import TestCase + +from model_bakery import baker + +class CustomerTestModel(TestCase): + + def setUp(self): + self.customer = baker.make( + 'shop.Customer', + age=21 + ) + + self.older_customer = baker.make( + 'shop.Customer', + age=42 + ) +``` + +You can use callable to explicitly set values as: + +```python +import random + +from django.test import TestCase + +from model_bakery import baker + +class CustomerTestModel(TestCase): + def get_random_name(self): + return random.choice(["Suraj Magdum", "Avadhut More", "Rohit Chile"]) + + def setUp(self): + self.customer = baker.make( + 'shop.Customer', + age=21, + name = self.get_random_name + ) +``` + +You can also use iterable to explicitly set values as: + +```python +from django.test import TestCase + +from model_bakery import baker + +class CustomerTestModel(TestCase): + def setUp(self): + names = ("Onkar Awale", "Pruthviraj Patil", "Shubham Ojha") + + self.customer = baker.make( + 'shop.Customer', + age=21, + name = itertools.cycle(names) + ) +``` + +Sometimes, you have a field with an unique value and using `make` can cause random errors. Also, passing an attribute value just to avoid uniqueness validation problems can be tedious. To solve this you can define a sequence with `seq` + +```python +from django.test import TestCase + +from model_bakery import baker + +from model_bakery.recipe import seq + +class CustomerTestModel(TestCase): + def setUp(self): + self.customer = baker.make( + 'shop.Customer', + age=21, + name = seq('Joe') + ) +``` + +Related objects fields are also reachable by their name or related names in a very similar way as Django does with [field lookups](https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups): + +```python +from django.test import TestCase + +from model_bakery import baker + +class PurchaseHistoryTestModel(TestCase): + + def setUp(self): + self.bob_history = baker.make( + 'shop.PurchaseHistory', + customer__name='Bob' + ) +``` + +## Creating Files + +Model Bakery does not create files for FileField types. If you need to have the files created, you can pass the flag `_create_files=True` (defaults to `False`) to either `baker.make` or `baker.make_recipe`. + +**Important**: the lib does not do any kind of file clean up, so it's up to you to delete the files created by it. + +## Non persistent objects + +If you don't need a persisted object, Model Bakery can handle this for you as well with the **prepare** method: + +```python +from model_bakery import baker + +customer = baker.prepare('shop.Customer') +``` + +It works like `make` method, but it doesn't persist the instance neither the related instances. + +If you want to persist only the related instances but not your model, you can use the `_save_related` parameter for it: + +```python +from model_bakery import baker + +history = baker.prepare('shop.PurchaseHistory', _save_related=True) +assert history.id is None +assert bool(history.customer.id) is True +``` + +## More than one instance + +If you need to create more than one instance of the model, you can use the `_quantity` parameter for it: + +```python +from model_bakery import baker + +customers = baker.make('shop.Customer', _quantity=3) +assert len(customers) == 3 +``` + +It also works with `prepare`: + +```python +from model_bakery import baker + +customers = baker.prepare('shop.Customer', _quantity=3) +assert len(customers) == 3 +``` + +The `make` method also accepts a parameter `_bulk_create` to use Django's [bulk_create](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#bulk-create) method instead of calling `obj.save()` for each created instance. + +```{note} +Django's `bulk_create` does not update the created object primary key as explained in their docs. Because of that, there's no way for model-bakery to avoid calling `save` method for all the foreign keys. But this behavior can depends on which Django version and database backend you're using. + +So, for example, if you're trying to create 20 instances of a model with a foreign key using `_bulk_create` this will result in 21 queries (20 for each foreign key object and one to bulk create your 20 instances). +``` + +If you want to avoid that, you'll have to perform individual bulk creations per foreign keys as the following example: + +```python +from model_bakery import baker + +baker.prepare(User, _quantity=5, _bulk_create=True) +user_iter = User.objects.all().iterator() +baker.prepare(Profile, user=user_iter, _quantity=5, _bulk_create=True) +``` + +## Multi-database support + +Model Bakery supports django application with more than one database. +If you want to determine which database bakery should use, you have the `_using` parameter: + +```python +from model_bakery import baker + +custom_db = "your_custom_db" +assert custom_db in settings.DATABASES +history = baker.make('shop.PurchaseHistory', _using=custom_db) +assert history in PurchaseHistory.objects.using(custom_db).all() +assert history.customer in Customer.objects.using(custom_db).all() +# default database tables with no data +assert not PurchaseHistory.objects.exists() +assert not Customer.objects.exists() +``` diff --git a/docs/source/conf.py b/docs/conf.py similarity index 66% rename from docs/source/conf.py rename to docs/conf.py index 638dd563..9150181b 100644 --- a/docs/source/conf.py +++ b/docs/conf.py @@ -1,7 +1,7 @@ import os import sys -sys.path.insert(0, os.path.abspath("../..")) +sys.path.insert(0, os.path.abspath("..")) from model_bakery import __about__ # noqa @@ -10,7 +10,15 @@ author = "Rust Saiargaliev" version = release = __about__.__version__ -extensions = [] +extensions = [ + "myst_parser", +] + +myst_enable_extensions = [ + "colon_fence", +] + +source_suffix = [".rst", ".md"] templates_path = ["_templates"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] diff --git a/docs/how_bakery_behaves.md b/docs/how_bakery_behaves.md new file mode 100644 index 00000000..fc7fc5aa --- /dev/null +++ b/docs/how_bakery_behaves.md @@ -0,0 +1,126 @@ +# How Model Bakery behaves? + +By default, Model Bakery skips fields with `null=True` or `blank=True`. Also if a field has a `default` value, it will be used. + +You can override this behavior by: + +1. Explicitly defining values + +```python +# from "Basic Usage" page, assume all fields either null=True or blank=True +from model_bakery import baker + +customer = baker.make('shop.Customer', enjoy_jards_macale=True, bio="A fan of Jards Malacé") +``` + +2. Passing `_fill_optional` with a list of fields to fill with random data + +```python +customer = baker.make('shop.Customer', _fill_optional=['enjoy_jards_macale', 'bio']) +``` + +3. Passing `_fill_optional=True` to fill all fields with random data + +```python +customer = baker.make('shop.Customer', _fill_optional=True) +``` + +## When shouldn't you let Baker generate things for you? + +If you have fields with special validation, you should set their values by yourself. + +Model Bakery should handle fields that: + +1. don't matter for the test you're writing; +2. don't require special validation (like unique, etc); +3. are required to create the object. + +## Currently supported fields + +- `BooleanField`, `NullBooleanField`, `IntegerField`, `BigIntegerField`, `SmallIntegerField`, `PositiveIntegerField`, `PositiveSmallIntegerField`, `FloatField`, `DecimalField` +- `CharField`, `TextField`, `BinaryField`, `SlugField`, `URLField`, `EmailField`, `IPAddressField`, `GenericIPAddressField`, `ContentType` +- `ForeignKey`, `OneToOneField`, `ManyToManyField` (even with through model) +- `DateField`, `DateTimeField`, `TimeField`, `DurationField` +- `FileField`, `ImageField` +- `JSONField`, `ArrayField`, `HStoreField` +- `CICharField`, `CIEmailField`, `CITextField` +- `DecimalRangeField`, `IntegerRangeField`, `BigIntegerRangeField`, `DateRangeField`, `DateTimeRangeField` + +Require `django.contrib.gis` in `INSTALLED_APPS`: + +- `GeometryField`, `PointField`, `LineStringField`, `PolygonField`, `MultiPointField`, `MultiLineStringField`, `MultiPolygonField`, `GeometryCollectionField` + +## Custom fields + +Model Bakery allows you to define generators methods for your custom fields or overrides its default generators. +This can be achieved by specifying the field and generator function for the `generators.add` function. +Both can be the real python objects imported in settings or just specified as import path string. + +Examples: + +```python +from model_bakery import baker + +def gen_func(): + return 'value' + +baker.generators.add('test.generic.fields.CustomField', gen_func) +``` + +```python +# in the module code.path: +def gen_func(): + return 'value' + +# in your tests.py file: +from model_bakery import baker + +baker.generators.add('test.generic.fields.CustomField', 'code.path.gen_func') +``` + +## Customizing Baker + +In some rare cases, you might need to customize the way Baker base class behaves. +This can be achieved by creating a new class and specifying it in your settings files. It is likely that you will want to extend Baker, however the minimum requirement is that the custom class have `make` and `prepare` functions. +In order for the custom class to be used, make sure to use the `model_bakery.baker.make` and `model_bakery.baker.prepare` functions, and not `model_bakery.baker.Baker` directly. + +Examples: + +```python +# in the module code.path: +class CustomBaker(baker.Baker): + def get_fields(self): + return [ + field + for field in super(CustomBaker, self).get_fields() + if not field isinstance CustomField + ] + +# in your settings.py file: +BAKER_CUSTOM_CLASS = 'code.path.CustomBaker' +``` + +Additionally, if you want to your created instance to be returned respecting one of your custom ModelManagers, you can use the `_from_manager` parameter as the example below: + +```python +movie = baker.make(Movie, title='Old Boys', _from_manager='availables') # This will use the Movie.availables model manager +``` + +## Save method custom parameters + +If you have overwritten the `save` method for a model, you can pass custom parameters to it using Model Bakery. Example: + +```python +class ProjectWithCustomSave(models.Model) + # some model fields + created_by = models.ForeignKey(settings.AUTH_USER_MODEL) + + def save(self, user, *args, **kwargs): + self.created_by = user + return super(ProjectWithCustomSave, self).save(*args, **kwargs) + +#with model baker: +user = baker.make(settings.AUTH_USER_MODEL) +project = baker.make(ProjectWithCustomSave, _save_kwargs={'user': user}) +assert user == project.user +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..3312422c --- /dev/null +++ b/docs/index.md @@ -0,0 +1,45 @@ +# Model Bakery: Smart fixtures for better tests + +Model Bakery offers you a smart way to create fixtures for testing in Django. + +With a simple and powerful API, you can create many objects with a single line of code. + +Model Bakery is a rename of the legacy [model_mommy\'s project](https://pypi.org/project/model_mommy/). This is because the project\'s creator and maintainers decided to not reinforce gender stereotypes for women in technology. You can read more about this subject[here](https://witi.com/articles/1017/How-Gender-Stereotypes-are-Still-Affecting-Women-in-Tech/). + +# Compatibility + +Model Bakery supports Django \>= 3.2. + +# Install + +Install it with `pip` + +```console +$ pip install model-bakery +``` + +# Contributing to Model Bakery + +As an open-source project, Model Bakery welcomes contributions of many forms. Examples of contributions include: + +- Code Patches +- Documentation improvements +- Bug reports + +Take a look in our [GitHub repo](https://github.com/model-bakers/model_bakery/blob/main/CONTRIBUTING.md) for more instructions on how to set up your local environment to help Model Bakery to grow. + +# Doubts? Loved it? Hated it? Suggestions? + +Feel free to [open an issue](https://github.com/model-bakers/model_bakery/issues/new) for support, development or ideas! + +## Contents + +```{toctree} +:maxdepth: 4 + +basic_usage +recipes +how_bakery_behaves +test_runners +migrating_from_mommy +``` diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 2c346bf7..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source -set I18NSPHINXOPTS=%SPHINXOPTS% source -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ModelMommy.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ModelMommy.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/docs/migrating_from_mommy.md b/docs/migrating_from_mommy.md new file mode 100644 index 00000000..5a542fbf --- /dev/null +++ b/docs/migrating_from_mommy.md @@ -0,0 +1,16 @@ +# Migrating from Model Mommy + +Model Bakery has a [Python script](https://github.com/model-bakers/model_bakery/blob/main/utils/from_mommy_to_bakery.py) to help you to migrate your project\'s test code from Model Mommy to Model Bakery. This script will rename recipe files and replace legacy imports by the new ones. + +**From your project\'s root dir**, execute the following commands: + +```console +$ pip uninstall model_mommy +$ pip install model_bakery +$ wget https://raw.githubusercontent.com/model-bakers/model_bakery/main/utils/from_mommy_to_bakery.py +$ python from_mommy_to_bakery.py --dry-run # will list the files that'll be changed +$ python from_mommy_to_bakery.py # migrate from model_mommy to model_bakery +$ python manage.py test +``` + +This command will only migrate `*.py` files. Any other file type such as `tox.ini`, `requirements.txt` etc, have to be updated manually. diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 00000000..4ea2b6e5 --- /dev/null +++ b/docs/recipes.md @@ -0,0 +1,361 @@ +# Recipes + +If you're not comfortable with random data or even if you just want to +improve the semantics of the generated data, there's hope for you. + +You can define a **Recipe**, which is a set of rules to generate data +for your models. + +It's also possible to store the Recipes in a module called *baker_recipes.py* +at your app's root directory. This recipes can later be used with the +`make_recipe` function: + +``` +shop/ + migrations/ + __init__.py + admin.py + apps.py + baker_recipes.py <--- where you should place your Recipes + models.py + tests.py + views.py +``` + +File: **baker_recipes.py** + +``` +from model_bakery.recipe import Recipe +from shop.models import Customer + +customer_joe = Recipe( + Customer, + name='John Doe', + nickname='joe', + age=18, + birthday=date.today(), + last_shopping=datetime.now() +) +``` + +:::{note} +You don't have to declare all the fields if you don't want to. Omitted fields will be generated automatically. +::: + +File: **test_model.py** + +``` +from django.test import TestCase + +from model_bakery import baker + +from shop.models import Customer, Contact + +class CustomerTestModel(TestCase): + + def setUp(self): + # Load the recipe 'customer_joe' from 'shop/baker_recipes.py' + self.customer_one = baker.make_recipe( + 'shop.customer_joe' + ) +``` + +Or if you don't want a persisted instance: + +``` +from model_bakery import baker + +baker.prepare_recipe('shop.customer_joe') +``` + +:::{note} +You don't have to place necessarily your `baker_recipes.py` file inside your app's root directory. +If you have a tests directory within the app, for example, you can add your recipes inside it and still +use `make_recipe`/`prepare_recipe` by adding the tests module to the string you've passed as an argument. +For example: `baker.make_recipe("shop.tests.customer_joe")` + +So, short summary, you can place your `baker_recipes.py` **anywhere** you want to and to use it having in mind +you'll only have to simulate an import but obfuscating the `baker_recipes` module from the import string. +::: + +:::{note} +You can use the \_quantity parameter as well if you want to create more than one object from a single recipe. +::: + +You can define recipes locally to your module or test case as well. This can be useful for cases where a particular set of values may be unique to a particular test case, but used repeatedly there. For example: + +File: **baker_recipes.py** + +``` +company_recipe = Recipe(Company, name='WidgetCo') +``` + +File: **test_model.py** + +``` +class EmployeeTest(TestCase): + def setUp(self): + self.employee_recipe = Recipe( + Employee, + name=seq('Employee '), + company=baker.make_recipe('app.company_recipe') + ) + + def test_employee_list(self): + self.employee_recipe.make(_quantity=3) + # test stuff.... + + def test_employee_tasks(self): + employee1 = self.employee_recipe.make() + task_recipe = Recipe(Task, employee=employee1) + task_recipe.make(status='done') + task_recipe.make(due_date=datetime(2014, 1, 1)) + # test stuff.... +``` + +## Recipes with foreign keys + +You can define `foreign_key` relations: + +```python +from model_bakery.recipe import Recipe, foreign_key +from shop.models import Customer, PurchaseHistory + +customer = Recipe(Customer, + name='John Doe', + nickname='joe', + age=18, + birthday=date.today(), + appointment=datetime.now() +) + +history = Recipe(PurchaseHistory, + owner=foreign_key(customer) +) +``` + +Notice that `customer` is a *recipe*. + +You may be thinking: "I can put the Customer model instance directly in the owner field". That's not recommended. + +Using the `foreign_key` is important for 2 reasons: + +- Semantics. You'll know that attribute is a foreign key when you're reading; +- The associated instance will be created only when you call `make_recipe` and not during recipe definition; + +You can also use `related`, when you want two or more models to share the same parent: + +```python +from model_bakery.recipe import related, Recipe +from shop.models import Customer, PurchaseHistory + +history = Recipe(PurchaseHistory) +customer_with_2_histories = Recipe(Customer, + name='Albert', + purchasehistory_set=related('history', 'history'), +) +``` + +Note this will only work when calling `make_recipe` because the related manager requires the objects in the related_set to be persisted. That said, calling `prepare_recipe` the related_set will be empty. + +If you want to set m2m relationship you can use `related` as well: + +```python +from model_bakery.recipe import related, Recipe + +pencil = Recipe(Product, name='Pencil') +pen = Recipe(Product, name='Pen') +history = Recipe(PurchaseHistory) + +history_with_prods = history.extend( + products=related(pencil, pen) +) +``` + +When creating models based on a `foreign_key` recipe using the `_quantity` argument, only one related model will be created for all new instances. + +```python +from model_baker.recipe import foreign_key, Recipe + +person = Recipe(Person, name='Albert') +dog = Recipe(Dog, owner=foreign_key(person)) + +# All dogs share the same owner +dogs = dog.make_recipe(_quantity=2) +assert dogs[0].owner.id == dogs[1].owner.id +``` + +This will cause an issue if your models use `OneToOneField`. In that case, you can provide `one_to_one=True` to the recipe to make sure every instance created by `_quantity` has a unique id. + +```python +from model_baker.recipe import foreign_key, Recipe + +person = Recipe(Person, name='Albert') +dog = Recipe(Dog, owner=foreign_key(person, one_to_one=True)) + +# Each dog has a unique owner +dogs = dog.make_recipe(_quantity=2) +assert dogs[0].owner.id != dogs[1].owner.id +``` + +## Recipes with callables + +It's possible to use `callables` as recipe's attribute value. + +```python +from datetime import date +from model_bakery.recipe import Recipe +from shop.models import Customer + +customer = Recipe( + Customer, + birthday=date.today, +) +``` + +When you call `make_recipe`, Model Bakery will set the attribute to the value returned by the callable. + +## Recipes with iterators + +You can also use *iterators* (including *generators*) to provide multiple values to a recipe. + +```python +from itertools import cycle + +names = ['Ada Lovelace', 'Grace Hopper', 'Ida Rhodes', 'Barbara Liskov'] +customer = Recipe(Customer, + name=cycle(names) +) +``` + +Model Bakery will use the next value in the *iterator* every time you create a model from the recipe. + +## Sequences in recipes + +Sometimes, you have a field with an unique value and using `make` can cause random errors. Also, passing an attribute value just to avoid uniqueness validation problems can be tedious. To solve this you can define a sequence with `seq` + +```python +>>> from model_bakery.recipe import Recipe, seq +>>> from shop.models import Customer + +>>> customer = Recipe(Customer, + name=seq('Joe'), + age=seq(15) +) + +>>> customer = baker.make_recipe('shop.customer') +>>> customer.name +'Joe1' +>>> customer.age +16 + +>>> new_customer = baker.make_recipe('shop.customer') +>>> new_customer.name +'Joe2' +>>> new_customer.age +17 +``` + +This will append a counter to strings to avoid uniqueness problems and it will sum the counter with numerical values. + +An optional `suffix` parameter can be supplied to augment the value for cases like generating emails +or other strings with common suffixes. + +```python +>>> from model_bakery import.recipe import Recipe, seq +>>> from shop.models import Customer + +>>> customer = Recipe(Customer, email=seq('user', suffix='@example.com')) + +>>> customer = baker.make_recipe('shop.customer') +>>> customer.email +'user1@example.com' + +>>> customer = baker.make_recipe('shop.customer') +>>> customer.email +'user2@example.com' +``` + +Sequences and iterables can be used not only for recipes, but with `baker` as well: + +```python +>>> from model_bakery import baker + +>>> customer = baker.make('Customer', name=baker.seq('Joe')) +>>> customer.name +'Joe1' + +>>> customers = baker.make('Customer', name=baker.seq('Chad'), _quantity=3) +>>> for customer in customers: +... print(customer.name) +'Chad1' +'Chad2' +'Chad3' +``` + +You can also provide an optional `increment_by` argument which will modify incrementing behaviour. This can be an integer, float, Decimal or timedelta. If you want to start your increment differently, you can use the `start` argument, only if it's not a sequence for `date`, `datetime` or `time` objects. + +```python +>>> from datetime import date, timedelta +>>> from model_bakery.recipe import Recipe, seq +>>> from shop.models import Customer + + +>>> customer = Recipe(Customer, + age=seq(15, increment_by=3) + height_ft=seq(5.5, increment_by=.25) + # assume today's date is 21/07/2014 + appointment=seq(date(2014, 7, 21), timedelta(days=1)), + name=seq('Custom num: ', increment_by=2, start=5), +) + +>>> customer = baker.make_recipe('shop.customer') +>>> customer.age +18 +>>> customer.height_ft +5.75 +>>> customer.appointment +datetime.date(2014, 7, 22) +>>> customer.name +'Custom num: 5' + +>>> new_customer = baker.make_recipe('shop.customer') +>>> new_customer.age +21 +>>> new_customer.height_ft +6.0 +>>> new_customer.appointment +datetime.date(2014, 7, 23) +>>> customer.name +'Custom num: 7' +``` + +Be aware that `seq` may query the database to determine when to reset. Therefore, a `SimpleTestCase` test method (which disallows database access) can call `prepare_recipe` on a Recipe with a `seq` once, but not not more than once within a test, even though the record itself is never saved to the database. + +## Overriding recipe definitions + +Passing values when calling `make_recipe` or `prepare_recipe` will override the recipe rule. + +```python +from model_bakery import baker + +baker.make_recipe('shop.customer', name='Ada Lovelace') +``` + +This is useful when you have to create multiple objects and you have some unique field, for instance. + +## Recipe inheritance + +If you need to reuse and override existent recipe call extend method: + +```python +customer = Recipe( + Customer, + bio='Some customer bio', + age=30, + enjoy_jards_macale=True, +) +sad_customer = customer.extend( + enjoy_jards_macale=False, +) +``` diff --git a/docs/source/basic_usage.rst b/docs/source/basic_usage.rst deleted file mode 100644 index 38e981fd..00000000 --- a/docs/source/basic_usage.rst +++ /dev/null @@ -1,306 +0,0 @@ -Basic Usage -=========== - -Let's say you have an app **shop** with a model like this: - -File: **models.py** :: - - class Customer(models.Model): - """ - Model class Customer of shop app - """ - enjoy_jards_macale = models.BooleanField() - name = models.CharField(max_length=30) - email = models.EmailField() - age = models.IntegerField() - bio = models.TextField() - days_since_last_login = models.BigIntegerField() - birthday = models.DateField() - last_shopping = models.DateTimeField() - -To create a persisted instance, just call Model Bakery: - -File: **test_models.py** :: - - #Core Django imports - from django.test import TestCase - - #Third-party app imports - from model_bakery import baker - - from shop.models import Customer - - class CustomerTestModel(TestCase): - """ - Class to test the model Customer - """ - - def setUp(self): - self.customer = baker.make(Customer) - -Importing every model over and over again is boring. So let Model Bakery import them for you: :: - - from model_bakery import baker - - # 1st form: app_label.model_name - customer = baker.make('shop.Customer') - - # 2nd form: model_name - product = baker.make('Product') - -.. note:: - - You can only use the 2nd form on unique model names. If you have an app shop with a Product, and an app stock with a Product, you must use the app_label.model_name form. - -.. note:: - - model_name is case insensitive. - -Model Relationships -------------------- - -Model Bakery also handles relationships. Let's say the customer has a purchase history: - -File: **models.py** :: - - class Customer(models.Model): - """ - Model class Customer of shop app - """ - enjoy_jards_macale = models.BooleanField() - name = models.CharField(max_length=30) - email = models.EmailField() - age = models.IntegerField() - bio = models.TextField() - days_since_last_login = models.BigIntegerField() - birthday = models.DateField() - appointment = models.DateTimeField() - - class PurchaseHistory(models.Model): - """ - Model class PurchaseHistory of shop app - """ - customer = models.ForeignKey('Customer') - products = models.ManyToManyField('Product') - year = models.IntegerField() - -You can use Model Bakery as: :: - - from django.test import TestCase - - from model_bakery import baker - - class PurchaseHistoryTestModel(TestCase): - - def setUp(self): - self.history = baker.make('shop.PurchaseHistory') - print(self.history.customer) - -It will also create the Customer, automagically. - -**NOTE: ForeignKeys and OneToOneFields** - Since Django 1.8, ForeignKey and OneToOne fields don't accept unpersisted model instances anymore. This means that if you run: :: - - baker.prepare('shop.PurchaseHistory') - -You'll end up with a persisted "Customer" instance. - -M2M Relationships ------------------ - -By default Model Bakery doesn't create related instances for many-to-many relationships. If you want them to be created, you have to turn it on as following: :: - - from django.test import TestCase - - from model_bakery import baker - - class PurchaseHistoryTestModel(TestCase): - - def setUp(self): - self.history = baker.make('shop.PurchaseHistory', make_m2m=True) - print(self.history.products.count()) - - -Explicit M2M Relationships --------------------------- -If you want to, you can prepare your own set of related object and pass it to Model Bakery. Here's an example: :: - - products_set = baker.prepare(Product, _quantity=5) - history = baker.make(PurchaseHistory, products=products_set) - - -Explicit values for fields --------------------------- - -By default, Model Bakery uses random values to populate the model's fields. But it's possible to explicitly set values for them as well. :: - - from django.test import TestCase - - from model_bakery import baker - - class CustomerTestModel(TestCase): - - def setUp(self): - self.customer = baker.make( - 'shop.Customer', - age=21 - ) - - self.older_customer = baker.make( - 'shop.Customer', - age=42 - ) - -You can use callable to explicitly set values as: :: - - import random - - from django.test import TestCase - - from model_bakery import baker - - class CustomerTestModel(TestCase): - def get_random_name(self): - return random.choice(["Suraj Magdum", "Avadhut More", "Rohit Chile"]) - - def setUp(self): - self.customer = baker.make( - 'shop.Customer', - age=21, - name = self.get_random_name - ) - -You can also use iterable to explicitly set values as: :: - - from django.test import TestCase - - from model_bakery import baker - - class CustomerTestModel(TestCase): - def setUp(self): - names = ("Onkar Awale", "Pruthviraj Patil", "Shubham Ojha") - - self.customer = baker.make( - 'shop.Customer', - age=21, - name = itertools.cycle(names) - ) - -Sometimes, you have a field with an unique value and using ``make`` can cause random errors. Also, passing an attribute value just to avoid uniqueness validation problems can be tedious. To solve this you can define a sequence with ``seq`` :: - - from django.test import TestCase - - from model_bakery import baker - - from model_bakery.recipe import seq - - class CustomerTestModel(TestCase): - def setUp(self): - self.customer = baker.make( - 'shop.Customer', - age=21, - name = seq('Joe') - ) - -Related objects fields are also reachable by their name or related names in a very similar way as Django does with `field lookups `_: :: - - from django.test import TestCase - - from model_bakery import baker - - class PurchaseHistoryTestModel(TestCase): - - def setUp(self): - self.bob_history = baker.make( - 'shop.PurchaseHistory', - customer__name='Bob' - ) - -Creating Files --------------- - -Model Bakery does not create files for FileField types. If you need to have the files created, you can pass the flag ``_create_files=True`` (defaults to ``False``) to either ``baker.make`` or ``baker.make_recipe``. - -**Important**: the lib does not do any kind of file clean up, so it's up to you to delete the files created by it. - - -Non persistent objects ----------------------- - -If you don't need a persisted object, Model Bakery can handle this for you as well with the **prepare** method: - -.. code-block:: python - - from model_bakery import baker - - customer = baker.prepare('shop.Customer') - -It works like ``make`` method, but it doesn't persist the instance neither the related instances. - -If you want to persist only the related instances but not your model, you can use the ``_save_related`` parameter for it: - -.. code-block:: python - - from model_bakery import baker - - history = baker.prepare('shop.PurchaseHistory', _save_related=True) - assert history.id is None - assert bool(history.customer.id) is True - -More than one instance ----------------------- - -If you need to create more than one instance of the model, you can use the ``_quantity`` parameter for it: - -.. code-block:: python - - from model_bakery import baker - - customers = baker.make('shop.Customer', _quantity=3) - assert len(customers) == 3 - -It also works with ``prepare``: - -.. code-block:: python - - from model_bakery import baker - - customers = baker.prepare('shop.Customer', _quantity=3) - assert len(customers) == 3 - -The ``make`` method also accepts a parameter ``_bulk_create`` to use Django's `bulk_create `_ method instead of calling ``obj.save()`` for each created instance. - -.. note:: - - Django's ``bulk_create`` does not update the created object primary key as explained in their docs. Because of that, there's no way for model-bakery to avoid calling ``save`` method for all the foreign keys. But this behavior can depends on which Django version and database backend you're using. - - So, for example, if you're trying to create 20 instances of a model with a foreign key using ``_bulk_create`` this will result in 21 queries (20 for each foreign key object and one to bulk create your 20 instances). - -If you want to avoid that, you'll have to perform individual bulk creations per foreign keys as the following example: - -.. code-block:: python - - from model_bakery import baker - - baker.prepare(User, _quantity=5, _bulk_create=True) - user_iter = User.objects.all().iterator() - baker.prepare(Profile, user=user_iter, _quantity=5, _bulk_create=True) - -Multi-database support ----------------------- - -Model Bakery supports django application with more than one database. -If you want to determine which database bakery should use, you have the ``_using`` parameter: - - -.. code-block:: python - - from model_bakery import baker - - custom_db = "your_custom_db" - assert custom_db in settings.DATABASES - history = baker.make('shop.PurchaseHistory', _using=custom_db) - assert history in PurchaseHistory.objects.using(custom_db).all() - assert history.customer in Customer.objects.using(custom_db).all() - # default database tables with no data - assert not PurchaseHistory.objects.exists() - assert not Customer.objects.exists() diff --git a/docs/source/how_bakery_behaves.rst b/docs/source/how_bakery_behaves.rst deleted file mode 100644 index 98c98840..00000000 --- a/docs/source/how_bakery_behaves.rst +++ /dev/null @@ -1,137 +0,0 @@ -How Model Bakery behaves? -========================= - -By default, Model Bakery skips fields with ``null=True`` or ``blank=True``. Also if a field has a ``default`` value, it will be used. - -You can override this behavior by: - -1. Explicitly defining values - -.. code-block:: python - - # from "Basic Usage" page, assume all fields either null=True or blank=True - from model_bakery import baker - - customer = baker.make('shop.Customer', enjoy_jards_macale=True, bio="A fan of Jards Malacé") - -2. Passing ``_fill_optional`` with a list of fields to fill with random data - -.. code-block:: python - - customer = baker.make('shop.Customer', _fill_optional=['enjoy_jards_macale', 'bio']) - -3. Passing ``_fill_optional=True`` to fill all fields with random data - -.. code-block:: python - - customer = baker.make('shop.Customer', _fill_optional=True) - - -When shouldn't you let Baker generate things for you? ------------------------------------------------------ - -If you have fields with special validation, you should set their values by yourself. - -Model Bakery should handle fields that: - -1. don't matter for the test you're writing; -2. don't require special validation (like unique, etc); -3. are required to create the object. - - -Currently supported fields --------------------------- - -* ``BooleanField``, ``NullBooleanField``, ``IntegerField``, ``BigIntegerField``, ``SmallIntegerField``, ``PositiveIntegerField``, ``PositiveSmallIntegerField``, ``FloatField``, ``DecimalField`` -* ``CharField``, ``TextField``, ``BinaryField``, ``SlugField``, ``URLField``, ``EmailField``, ``IPAddressField``, ``GenericIPAddressField``, ``ContentType`` -* ``ForeignKey``, ``OneToOneField``, ``ManyToManyField`` (even with through model) -* ``DateField``, ``DateTimeField``, ``TimeField``, ``DurationField`` -* ``FileField``, ``ImageField`` -* ``JSONField``, ``ArrayField``, ``HStoreField`` -* ``CICharField``, ``CIEmailField``, ``CITextField`` -* ``DecimalRangeField``, ``IntegerRangeField``, ``BigIntegerRangeField``, ``DateRangeField``, ``DateTimeRangeField`` - -Require ``django.contrib.gis`` in ``INSTALLED_APPS``: - -* ``GeometryField``, ``PointField``, ``LineStringField``, ``PolygonField``, ``MultiPointField``, ``MultiLineStringField``, ``MultiPolygonField``, ``GeometryCollectionField`` - -Custom fields -------------- - -Model Bakery allows you to define generators methods for your custom fields or overrides its default generators. -This can be achieved by specifying the field and generator function for the ``generators.add`` function. -Both can be the real python objects imported in settings or just specified as import path string. - -Examples: - -.. code-block:: python - - from model_bakery import baker - - def gen_func(): - return 'value' - - baker.generators.add('test.generic.fields.CustomField', gen_func) - -.. code-block:: python - - # in the module code.path: - def gen_func(): - return 'value' - - # in your tests.py file: - from model_bakery import baker - - baker.generators.add('test.generic.fields.CustomField', 'code.path.gen_func') - -Customizing Baker ------------------ - -In some rare cases, you might need to customize the way Baker base class behaves. -This can be achieved by creating a new class and specifying it in your settings files. It is likely that you will want to extend Baker, however the minimum requirement is that the custom class have ``make`` and ``prepare`` functions. -In order for the custom class to be used, make sure to use the ``model_bakery.baker.make`` and ``model_bakery.baker.prepare`` functions, and not ``model_bakery.baker.Baker`` directly. - -Examples: - -.. code-block:: python - - # in the module code.path: - class CustomBaker(baker.Baker): - def get_fields(self): - return [ - field - for field in super(CustomBaker, self).get_fields() - if not field isinstance CustomField - ] - - # in your settings.py file: - BAKER_CUSTOM_CLASS = 'code.path.CustomBaker' - - -Additionally, if you want to your created instance to be returned respecting one of your custom ModelManagers, you can use the ``_from_manager`` parameter as the example below: - - -.. code-block:: python - - movie = baker.make(Movie, title='Old Boys', _from_manager='availables') # This will use the Movie.availables model manager - - -Save method custom parameters ------------------------------ - -If you have overwritten the ``save`` method for a model, you can pass custom parameters to it using Model Bakery. Example: - -.. code-block:: python - - class ProjectWithCustomSave(models.Model) - # some model fields - created_by = models.ForeignKey(settings.AUTH_USER_MODEL) - - def save(self, user, *args, **kwargs): - self.created_by = user - return super(ProjectWithCustomSave, self).save(*args, **kwargs) - - #with model baker: - user = baker.make(settings.AUTH_USER_MODEL) - project = baker.make(ProjectWithCustomSave, _save_kwargs={'user': user}) - assert user == project.user diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 02279fb4..00000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,53 +0,0 @@ -Model Bakery: Smart fixtures for better tests -============================================= - -Model Bakery offers you a smart way to create fixtures for testing in Django. - -With a simple and powerful API you can create many objects with a single line of code. - -Model Bakery is a rename of the legacy `model_mommy's project `_. This is because the project's creator and maintainers decided to not reinforce gender stereotypes for women in technology. You can read more about this subject `here `_. - - -Compatibility -============= - -Model Bakery supports Django >= 3.2. - -Install -======= - -Install it with ``pip`` - -.. code-block:: console - - $ pip install model-bakery - - -Contributing to Model Bakery -============================ - -As an open source project, Model Bakery welcomes contributions of many forms. Examples of contributions include: - -* Code Patches -* Documentation improvements -* Bug reports - -Take a look in our `Github repo `_ for more instructions on how to set up your local environment to help Model Bakery to grow. - -Doubts? Loved it? Hated it? Suggestions? -======================================== - -Feel free to `open an issue `_ for support, development or ideas! - - -Contents --------- - -.. toctree:: - :maxdepth: 4 - - basic_usage - recipes - how_bakery_behaves - test_runners - migrating_from_mommy diff --git a/docs/source/migrating_from_mommy.rst b/docs/source/migrating_from_mommy.rst deleted file mode 100644 index 70a92b1c..00000000 --- a/docs/source/migrating_from_mommy.rst +++ /dev/null @@ -1,18 +0,0 @@ -Migrating from Model Mommy -========================== - -Model Bakery has a `Python script `_ to help you to migrate your project's test code from Model Mommy to Model Bakery. This script will rename recipe files and replace legacy imports by the new ones. - -**From your project's root dir**, execute the following commands: - -.. code-block:: console - - $ pip uninstall model_mommy - $ pip install model_bakery - $ wget https://raw.githubusercontent.com/model-bakers/model_bakery/main/utils/from_mommy_to_bakery.py - $ python from_mommy_to_bakery.py --dry-run # will list the files that'll be changed - $ python from_mommy_to_bakery.py # migrate from model_mommy to model_bakery - $ python manage.py test - - -This command will only migrate ``*.py`` files. Any other file type such as ``tox.ini``, ``requirements.txt`` etc, have to be updated manually. diff --git a/docs/source/recipes.rst b/docs/source/recipes.rst deleted file mode 100644 index db400100..00000000 --- a/docs/source/recipes.rst +++ /dev/null @@ -1,365 +0,0 @@ -Recipes -======= - -If you're not comfortable with random data or even if you just want to -improve the semantics of the generated data, there's hope for you. - -You can define a **Recipe**, which is a set of rules to generate data -for your models. - -It's also possible to store the Recipes in a module called *baker_recipes.py* -at your app's root directory. This recipes can later be used with the -``make_recipe`` function: :: - - shop/ - migrations/ - __init__.py - admin.py - apps.py - baker_recipes.py <--- where you should place your Recipes - models.py - tests.py - views.py - - -File: **baker_recipes.py** :: - - from model_bakery.recipe import Recipe - from shop.models import Customer - - customer_joe = Recipe( - Customer, - name='John Doe', - nickname='joe', - age=18, - birthday=date.today(), - last_shopping=datetime.now() - ) - -.. note:: - - You don't have to declare all the fields if you don't want to. Omitted fields will be generated automatically. - - -File: **test_model.py** :: - - from django.test import TestCase - - from model_bakery import baker - - from shop.models import Customer, Contact - - class CustomerTestModel(TestCase): - - def setUp(self): - # Load the recipe 'customer_joe' from 'shop/baker_recipes.py' - self.customer_one = baker.make_recipe( - 'shop.customer_joe' - ) - - -Or if you don't want a persisted instance: :: - - from model_bakery import baker - - baker.prepare_recipe('shop.customer_joe') - - -.. note:: - - You don't have to place necessarily your ``baker_recipes.py`` file inside your app's root directory. - If you have a tests directory within the app, for example, you can add your recipes inside it and still - use ``make_recipe``/``prepare_recipe`` by adding the tests module to the string you've passed as an argument. - For example: ``baker.make_recipe("shop.tests.customer_joe")`` - - So, short summary, you can place your ``baker_recipes.py`` **anywhere** you want to and to use it having in mind - you'll only have to simulate an import but obfuscating the ``baker_recipes`` module from the import string. - - -.. note:: - - You can use the _quantity parameter as well if you want to create more than one object from a single recipe. - - -You can define recipes locally to your module or test case as well. This can be useful for cases where a particular set of values may be unique to a particular test case, but used repeatedly there. For example: - -File: **baker_recipes.py** :: - - company_recipe = Recipe(Company, name='WidgetCo') - -File: **test_model.py** :: - - class EmployeeTest(TestCase): - def setUp(self): - self.employee_recipe = Recipe( - Employee, - name=seq('Employee '), - company=baker.make_recipe('app.company_recipe') - ) - - def test_employee_list(self): - self.employee_recipe.make(_quantity=3) - # test stuff.... - - def test_employee_tasks(self): - employee1 = self.employee_recipe.make() - task_recipe = Recipe(Task, employee=employee1) - task_recipe.make(status='done') - task_recipe.make(due_date=datetime(2014, 1, 1)) - # test stuff.... - -Recipes with foreign keys -------------------------- - -You can define ``foreign_key`` relations: - -.. code-block:: python - - from model_bakery.recipe import Recipe, foreign_key - from shop.models import Customer, PurchaseHistory - - customer = Recipe(Customer, - name='John Doe', - nickname='joe', - age=18, - birthday=date.today(), - appointment=datetime.now() - ) - - history = Recipe(PurchaseHistory, - owner=foreign_key(customer) - ) - -Notice that ``customer`` is a *recipe*. - -You may be thinking: "I can put the Customer model instance directly in the owner field". That's not recommended. - -Using the ``foreign_key`` is important for 2 reasons: - -* Semantics. You'll know that attribute is a foreign key when you're reading; -* The associated instance will be created only when you call ``make_recipe`` and not during recipe definition; - -You can also use ``related``, when you want two or more models to share the same parent: - -.. code-block:: python - - from model_bakery.recipe import related, Recipe - from shop.models import Customer, PurchaseHistory - - history = Recipe(PurchaseHistory) - customer_with_2_histories = Recipe(Customer, - name='Albert', - purchasehistory_set=related('history', 'history'), - ) - -Note this will only work when calling ``make_recipe`` because the related manager requires the objects in the related_set to be persisted. That said, calling ``prepare_recipe`` the related_set will be empty. - -If you want to set m2m relationship you can use ``related`` as well: - -.. code-block:: python - - from model_bakery.recipe import related, Recipe - - pencil = Recipe(Product, name='Pencil') - pen = Recipe(Product, name='Pen') - history = Recipe(PurchaseHistory) - - history_with_prods = history.extend( - products=related(pencil, pen) - ) - -When creating models based on a ``foreign_key`` recipe using the ``_quantity`` argument, only one related model will be created for all new instances. - -.. code-block:: python - - from model_baker.recipe import foreign_key, Recipe - - person = Recipe(Person, name='Albert') - dog = Recipe(Dog, owner=foreign_key(person)) - - # All dogs share the same owner - dogs = dog.make_recipe(_quantity=2) - assert dogs[0].owner.id == dogs[1].owner.id - -This will cause an issue if your models use ``OneToOneField``. In that case, you can provide ``one_to_one=True`` to the recipe to make sure every instance created by ``_quantity`` has a unique id. - -.. code-block:: python - - from model_baker.recipe import foreign_key, Recipe - - person = Recipe(Person, name='Albert') - dog = Recipe(Dog, owner=foreign_key(person, one_to_one=True)) - - # Each dog has a unique owner - dogs = dog.make_recipe(_quantity=2) - assert dogs[0].owner.id != dogs[1].owner.id - - - -Recipes with callables ----------------------- - -It's possible to use ``callables`` as recipe's attribute value. - -.. code-block:: python - - from datetime import date - from model_bakery.recipe import Recipe - from shop.models import Customer - - customer = Recipe( - Customer, - birthday=date.today, - ) - -When you call ``make_recipe``, Model Bakery will set the attribute to the value returned by the callable. - - -Recipes with iterators ----------------------- - -You can also use *iterators* (including *generators*) to provide multiple values to a recipe. - -.. code-block:: python - - from itertools import cycle - - names = ['Ada Lovelace', 'Grace Hopper', 'Ida Rhodes', 'Barbara Liskov'] - customer = Recipe(Customer, - name=cycle(names) - ) - -Model Bakery will use the next value in the *iterator* every time you create a model from the recipe. - -Sequences in recipes --------------------- - -Sometimes, you have a field with an unique value and using ``make`` can cause random errors. Also, passing an attribute value just to avoid uniqueness validation problems can be tedious. To solve this you can define a sequence with ``seq`` - -.. code-block:: python - - >>> from model_bakery.recipe import Recipe, seq - >>> from shop.models import Customer - - >>> customer = Recipe(Customer, - name=seq('Joe'), - age=seq(15) - ) - - >>> customer = baker.make_recipe('shop.customer') - >>> customer.name - 'Joe1' - >>> customer.age - 16 - - >>> new_customer = baker.make_recipe('shop.customer') - >>> new_customer.name - 'Joe2' - >>> new_customer.age - 17 - -This will append a counter to strings to avoid uniqueness problems and it will sum the counter with numerical values. - -An optional ``suffix`` parameter can be supplied to augment the value for cases like generating emails -or other strings with common suffixes. - -.. code-block:: python - - >>> from model_bakery import.recipe import Recipe, seq - >>> from shop.models import Customer - - >>> customer = Recipe(Customer, email=seq('user', suffix='@example.com')) - - >>> customer = baker.make_recipe('shop.customer') - >>> customer.email - 'user1@example.com' - - >>> customer = baker.make_recipe('shop.customer') - >>> customer.email - 'user2@example.com' - -Sequences and iterables can be used not only for recipes, but with ``baker`` as well: - -.. code-block:: python - - >>> from model_bakery import baker - - >>> customer = baker.make('Customer', name=baker.seq('Joe')) - >>> customer.name - 'Joe1' - - >>> customers = baker.make('Customer', name=baker.seq('Chad'), _quantity=3) - >>> for customer in customers: - ... print(customer.name) - 'Chad1' - 'Chad2' - 'Chad3' - -You can also provide an optional ``increment_by`` argument which will modify incrementing behaviour. This can be an integer, float, Decimal or timedelta. If you want to start your increment differently, you can use the ``start`` argument, only if it's not a sequence for ``date``, ``datetime`` or ``time`` objects. - -.. code-block:: python - - >>> from datetime import date, timedelta - >>> from model_bakery.recipe import Recipe, seq - >>> from shop.models import Customer - - - >>> customer = Recipe(Customer, - age=seq(15, increment_by=3) - height_ft=seq(5.5, increment_by=.25) - # assume today's date is 21/07/2014 - appointment=seq(date(2014, 7, 21), timedelta(days=1)), - name=seq('Custom num: ', increment_by=2, start=5), - ) - - >>> customer = baker.make_recipe('shop.customer') - >>> customer.age - 18 - >>> customer.height_ft - 5.75 - >>> customer.appointment - datetime.date(2014, 7, 22) - >>> customer.name - 'Custom num: 5' - - >>> new_customer = baker.make_recipe('shop.customer') - >>> new_customer.age - 21 - >>> new_customer.height_ft - 6.0 - >>> new_customer.appointment - datetime.date(2014, 7, 23) - >>> customer.name - 'Custom num: 7' - -Be aware that ``seq`` may query the database to determine when to reset. Therefore, a ``SimpleTestCase`` test method (which disallows database access) can call ``prepare_recipe`` on a Recipe with a ``seq`` once, but not not more than once within a test, even though the record itself is never saved to the database. - -Overriding recipe definitions ------------------------------ - -Passing values when calling ``make_recipe`` or ``prepare_recipe`` will override the recipe rule. - -.. code-block:: python - - from model_bakery import baker - - baker.make_recipe('shop.customer', name='Ada Lovelace') - -This is useful when you have to create multiple objects and you have some unique field, for instance. - -Recipe inheritance ------------------- - -If you need to reuse and override existent recipe call extend method: - -.. code-block:: python - - customer = Recipe( - Customer, - bio='Some customer bio', - age=30, - enjoy_jards_macale=True, - ) - sad_customer = customer.extend( - enjoy_jards_macale=False, - ) diff --git a/docs/source/test_runners.rst b/docs/test_runners.md similarity index 67% rename from docs/source/test_runners.rst rename to docs/test_runners.md index f97503fc..147abef5 100644 --- a/docs/source/test_runners.rst +++ b/docs/test_runners.md @@ -1,16 +1,14 @@ -Test Runners -============ +# Test Runners -Most of the code examples shown so far have used the `Django TestCase `_ to explain how Model Bakery is used. +Most of the code examples shown so far have used the [Django TestCase](https://docs.djangoproject.com/en/dev/topics/testing/tools/#testcase) to explain how Model Bakery is used. -However `pytest `_ (with the `pytest-django `_ plugin) is often preferred for it's simplicity and other benefits. See `here `_. +However, [pytest](https://docs.pytest.org/en/stable/) (with the [pytest-django](https://pytest-django.readthedocs.io/en/latest/) plugin) is often preferred for it\'s simplicity and other benefits. See [here](https://realpython.com/django-pytest-fixtures/). The following examples show Model Bakery usage with different test runners. -Django ------- -:: +## Django +```python # Core Django imports from django.test import TestCase @@ -31,11 +29,11 @@ Django def test_using_customer(self): """Test function using baked model.""" self.assertIsInstance(self.customer, Customer) +``` -pytest ------- -:: +## pytest +```python # pytest import import pytest @@ -52,3 +50,4 @@ pytest def test_using_customer(customer): """Test function using fixture of baked model.""" assert isinstance(customer, Customer) +``` diff --git a/pyproject.toml b/pyproject.toml index 957cf9bc..3d651e22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ test = [ docs = [ "Sphinx", "sphinx-rtd-theme", + "myst-parser", ] [project.urls]