Package: django-singleton2
The appInstallationConfigurationUsageThe process of creating the appOperating SystemProject Structuresetup a sensible environment (part 1)pyenvpoetrytoxpytestblacksphinxbootstrap your repositorybootstrapping the appboostrapping the project to develop againstbootstrapping the documentationsetup a sensible environment (part 2)codingconnect the app to the example-projectdocumenting the codetesting the codeversioning the codepackaging & publishing the codecontinuous integration (workflows)I AM HERE-tox-github actionspublishThe appA singleton is a class that can only be instanciated a single time.SingletonMixinturns any Django Model into a singleton.InstallationInstall with pip or your favorite Python package manager.pip install django-singleton2Add "singleton2" to yourINSTALLED_APPSINSTALLED_APPS = [
  ...
  'singleton2',
]ConfigurationUpdatesettings.pyif you feel like it:SINGLETON_RAISE_ERROR_ON_SAVE = True|FalseThe default value is False.Usagefrom django.db import models
from singleton2.models import SingletonMixin

class MySingletonModel(SingletonMixin, models.Model):
  pass

my_singleton_instance = MySingletonModel.load()Validation errors will ocurr if you try to save more than one instance of a singleton via a form (such as in the Django Admin).  These will be handled by normal Django form processing.If you try to save more than one instance of a singleton outside of a form then nothing will happen, unlessSINGLETON_RAISE_ERROR_ON_SAVEis set to True in which case an exception will be raised.Notethat the app is called "django-singleton2" because there is an existingdjango-singletonapp which does the same thing and works perfectly well.The process of creating the appAll the stuff that other people seem to already know is described here.Operating SystemI do all of my coding on Linux because I'm not insane.  I use Xubuntu because Ubuntu is pretty standard, but I'm used to having lousy hardware and XFCE takes less processing power than Unity.  Plus, I'm used to it.These days, a lot of development is done in Docker.  So the underlying OS doesn't really matter.  Still, if I can avoid virutalization I will.  This Django Reusable App does not rely on Docker for development purposes.Project StructureFirst create the repository in GiHub.  In theory this can be done from the command line usinggh, but honestly, I find using the GitHub website GUI a lot more straightforward.  Plus doing it this way can automatically create the "README.md" and "LICENCE" files for you.Once that has been done, clone this repository into your local filesystem and then setup the basic directory structure:git clone git@github.com:allynt/django-singleton2.git django-singleton
mkdir -p django-singleton/singleton2
mkdir -p django-singleton/example-project
mkdir -p django-singleton/docs
mkdir -p django-singleton/.github
cd django-singleton/This provides us with a place to store the app, an example project to develop against, documentation, and workflows for CI.Note that the repository already comes with a ".gitingore" file.  This has been generated fromgitignore.iowith a few extra bits tacked onto the end:# static & media files
example-project/_static/*
example-project/_media/*

# pytest output
example-project/pytest-report.html
example-project/output.json
example-project/archive

# local environment variables
.env.*

# in progress files
*.bak
*.origYou should override this according to your needs.Now that that's all out of the way rungit add .
git commit -m "initial commit"
git pushto make sure that github and your local filesystem are in sync.Since you're using github, now would be as good a time as any to create some templates to help potential collaborators.  I have created issue templates for "bug_reports" and "feature_requests" as well as a pull request template.  Again, this could have been done by manually creating files in ".github" but I find working with the GitHub GUI simpler (and it lets me build upon their existing templates).I also created a CODE_OF_CONDUCT.md file (as perhere) and a CONTRIBUTING.md file (as perhere).Now that that's all out of the way rungit pullto make sure that github and your local filesystem are in syncagain.setup a sensible environment (part 1)In order to develop code efficiently I use some standard Python tooling.pyenvI usepyenvto manage my Python versions.To install pyenv first make sure the dependencies are installed:sudo apt-get install -y make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev \
libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python-opensslThen install pyenv itself:$ curl https://pyenv.run | bashand add the following to "~/.bashrc":export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"Reload the shell and install your favorite versions of python.  Then instruct pyenv which version to use in the repository; I'm using "3.10.0" at the time of writing.pyenv local 3.10.0This should create a "django-singleton2/.python-version" file.poetryPython package management is hard.  There is also no one standard way of doing it.  Like most developers, I started withpipand migrated topipenvand toyed withpiptoolsand embracedpoetryand am intrigued bypdm(but dissappointed thatPEP582was not endorsed - that would have made working w/ code insideandoutside Docker containers much easier).  Poetry handles virtual environments and package dependency management as well as building and publishing code.  To install it simply run:curl -sSL https://install.python-poetry.org | python3 -
poetry self update
poetry completions bash >> ~/.bash_completionNote that the resultant completions file doesn’t work in all versions of bash; multi-word case statements have to be encased in quotes.Next setup some config:poetry config virtualenvs.in-project true`This ensures that all virtual environments will be located in the project directory in a directory called ".venv".  This is very useful for integrating your virtual environments with your IDEs as we'll see later on.From here on, when I instruct you to install a specific python package you should use poetry to be doing this.  All the installed packages will be recorded in a "pyproject.toml" file.  As you develop it is a good idea to generate a "poetry.lock" file that locks all dependencies to a specific version.I would say that this is essential when developing a Django Project but not when developing a Django Reusable App.  Anybody that uses it will incorporate it into their own "poetry.lock" file.  And, anyway, tox will reinstall dependencies during testing as we'll see later.Don't forget to runpoetry initto create your virtual environment and get the whole process started.toxToxallows me to test my app across multiple library versions.  When writing Django Resuable Apps, the main libraries you care about are Python and Django, obviously.  If this were a more complex app, I might choose to test against multiple versions of other libraries such as Django Rest Framework.Installing tox with poetry is very straightforward:poetry add --group dev tox="*"Note that this installs it as a "dev" dependency which means that poetry will not bundle the tox package when building districution of our app.tox is configured with a tox.ini file.  For a start I recommend:[tox]
envlist = py{38,310}-django{32,40,41}
isolated_build = true

[testenv]
allowlist_externals = 
    poetry
deps =
    pytest
commands_pre = 
    poetry install --no-root --sync
commands =
    poetry run pytest ./example-projectI will add to this file throughout this document.  And I will write more about tox in the testing section.pytestDjango comes with its own testing framework.  And it's pretty good.  If you are content with it that's fine.  I like to have a bit more control over my test runners.  And I like the way that pytest uses fixtures.  And I've just gotten used to it over the years.  So I usepytest.poetry add --group dev pytest pytest-django pytest-html-reporter factory-boyThere are several usefulpytest pluginsyou can add if you like.I'll write more about pytest in the testing section.blackI useblackfor linting because I like my code to be pretty.  Install it like this:poetry add --group dev blackI have usually usedyapf(Yet Another Python Formatter) because it can be configured and there are just a few personal stylistic idiosynchrasies that bug me.  But when writing a Django Reusable App where other developers might contribute, I concede that it makes sense to use something that can't be configured and therefore makes it impossible to get into arguments about coding style.  Hence,black.sphinxI usesphinxfor documenting my code.  This will automatically parse docstings in code as follows::class MyClass(object):
  """
  I am a docstring for the class
  """

  def my_function(self, *args, **kwargs):
    """
    I am another docstring for the function
    """
    pass

  
  my_variable = True
  """
  I am yet another docstring for the variable
  (notice how I come after the definition)
  """
  
  passWhen I have comments that I don't need to be documented, I use the following syntax:def my_complex_function(*args, **kwargs):
  for i in range(100):
    # this is a complex process that I want to leave a comment about, but I don't need sphinx to parse it
    do_something(i)Install sphinx, and some associated packages that I like, usingpoetry add --group docs sphinx sphinx-rtd-theme myst-parserBy default sphinx can parse ReStructuredText documents.myst-parserallows it to parse MarkDown (which is not only more intuitive, it is also what GitHub wrote your README file in).  Andsphinx-rtd-themeincorporates the standard theme used by ReadTheDocs - which is where our documentation is going to wind up (assuming this app is going to be public).bootstrap your repositoryRecall that we created some empty directories at the root of our repository.  We can now flesh them out with actual content.  I have created several templates that can be used to help with this process.  If you don't like my templates, then just use the standard bootstrapping tools.  If you find my templates too complex, then just copy this repository and change the bits you don't like.bootstrapping the appThis is easy.  Just run:django-admin startapp --template=https://github.com/allynt/django-reusable-app-template/archive/master.zip singleton2and the "singleton2" directory will be populated with the stubs needed to develop your app.This is a pretty standard Django App with the following tweaks:"apps.py" tries to register any checks and signals associated w/ the app"checks.py" includes some standard checks I run against all my apps; these can be expanded as needed"conf.py" includes app-specific configuration using thedjango-appconfreusable app"urls.py" includes an emptyurlpatternsarray that will be referenced by the example-project belowboostrapping the project to develop againstThis is also easy:django-admin startproject --template=https://github.com/allynt/django-reusable-app-project-template/archive/master.zip example example-projectto populate the "example-project" directory.When developing a Resuable Django App, there are different approaches to testing and building migrations.  I've come across a lot of apps that just don't document this at all.  I've come across others that have special scripts whichfakea Django Project (ie: just a minimal "settings.py" file) to rundjango.setup()with so that the app can be loaded in order to generate migrations.  But I feel like it makes sense to use arealDjango Project so that you can interact with your app naturally.Therefore the "example-project" that I create can be run just like any other Django Project.  It will allow me to generate migrations.  And it will allow me to run tests (located in "example-project/example/tests").  But, crucially, it will also allow me to hack about and make sure I understand exactly how the app works.I am quite partial to many of the ideas recommended by Cookiecuter Django and so this example-project uses some of their ideas.a separate app called "config" where all project-level configurations like "settings.py" and "urls.py" goan app called "example" where all the code that will use the Django Resuable App goes - this includes a "tests" modulesome custom additions to the "settings.py" file, including the use ofdjango-environto read environment variablesbootstrapping the documentationAs mentioned above I use sphinx for the documentation.  I have provided a template that tweaks the default documentation structure - including linking the README.md file and autogenerated documentation to the index page.  This template is available athttps://github.com/allynt/django-reusable-app-sphinx-template.  At the time of writing, the sphinx CLI cannot use a remote template, therefore this repository must first be cloned locally and then the following commandcd docs && sphinx-quickstart --templatedir=django-reusable-app-sphinx-template/templates .will create some initial documentation.You can add any additional documentation files you want within the "doc" directory.  Just make sure to include a reference to it underneath the:tocdirective in "index.rst".setup a sensible environment (part 2)An IDE makes life a lot easier.  I useVSCode.  I have therefore included a ".vscode/settings.json" file (and a generic ".editorconfig" file) to configure a few nice features.  My configuration includes a path to the virtual environment created by poetry (".venv") in order to do code-completion as well as linting.In theory, keeping these files with the repository not only makes it easier formeto code, it makes it less likely foryouto push code that conflicts with my style.TODOAlthough, I may also add a github action to automatically lint all incoming code.I also have some particularly useful VSCode plugins installed:MS Python (this will install loads of other useful plugins)Markdown all in oneBetter commentsBetter ToMLDotENVIntellicodeTabnineActually, I use a few more plugins for full-stack development, but these are the ones that are relevant for creating Django Resuable Apps.codingSo that is most of the setup required before finally diving into actual coding.  There are a few other things that are needed to actually run, test, document, publish, etc. the code.connect the app to the example-projectObviously, we need to tellexample-projectaboutdjango-singleton2.  This is very straightforward, just add the following to "example-project/config/settings.py":LOCAL_APPS = [
    "singleton2",
    "example",
]If your app is more complex then there will probably be some additional configuration needed, such as adding your views to "example-project/config/urls.py", etc.You also need to make sure that all of your app's dependencies are installed:poetry add django django-appconf django-environNow try runningpoetry run ./example-project/manage.py runserverand see if something appears at "localhost:8000".  If not, you messed up.documenting the codeI have already discussed usingsphinxto automatically generate some nice documentation.  I am going to host my documentation on readthedocs because I deserve it.  This requires registering the repository with readthedocs.  There is probably a way to do this on the command line, but I just signed up for an account on readthedocs.org and then manaually used their GUI to import this repostiroy.Having done that, it might look pretty if you add their badge to the README.md file:[![Documentation Status](https://readthedocs.org/projects/django-singleton2/badge/?version=latest)](https://django-singleton2.readthedocs.io/en/latest/?badge=latest)I am using tox to generate documentation becausewhy not?  I have added a separate section to the "tox.ini" file to deal w/ this.  Now, theofficial tox documentationrecommends bypassing the Makefile generated bysphinx-quickstartbut I disagree.  Whywouldn'tI want to continue using the official approved way of generating documentation?  All I want to do w/ tox is to eventually integrate this into CI and just runpoetry run tox -e docs.Anyway, doing things the "official" way got pretty complicated b/c of dependencies.  So I addedmaketo the commands in "allowlist_externals" and added the following section:[testenv:docs]
deps = 
    sphinx
    sphinx-rtd-theme
    myst-parser
changedir = docs
commands = 
    make clean
    make html SPHINXOPTS="-v"In order for this to work I had to create a ".readthedocs.yml" file which points to "docs/requirements.txt" in order to specify exactly what dependencies are required to run the codebase (since it needs to be loaded for auto-generated documentation).  Although readthedocscanparse the "pyproject.toml" file, it ignores grouped dependencies.  So to populate "docs/requirements.txt" I ranpoetry export -f requirments.txt --only docs --output docs/requirements.txt.Having done all of this will result inhttps://django-singleton2.readthedocs.iobeing auto-generated whenever the master branch is pushed to.If you want additional auto-generated code documentation, just modify "docs/singleton2.rst" to specify which modules / classes / methods should be included.testing the codeAs mentioned earlier, I usepytestas my test runner andtoxto coordinate testing across multiple environments.  This all requires a bit more config.  First make surepytest-djangoandfactory-boyare installed.  These make pytest work a bit nicer w/ Django.  The general syntax I use for a test is:TODOAdd test artefacts from pytest-html-reporterimport factory
import pytest

from example.models import ExampleSingletonModel

class ExampleSingletonModelFactory(factory.django.DjangoModelFactory):
    """
    creates an instance of ExampleSingletonModel w/ random field values to use in tests
    """
    class Meta:
        model = ExampleSingletonModel

    name = factory.Faker("word")

@pytest.mark.django_db
class TestSingleton:
    """
    collection of test methods that hit the Django database
    """
    def test_something(self):
        singleton = ExampleSingletonModelFactory()
        assert singleton_1.pk is not NoneUsing pytest requires a "pytest.ini" file w/ the following minimal content:[pytest]
DJANGO_SETTINGS_MODULE=config.settings
python_files = tests.py test_*.py *_tests.py
addopts = --nomigrationsThis tells pytest how to start Django and which files to run. (It also instructs it not to bother running any unmigrated migrations.)To run the tests simply run the following command:poetry run pytest ./example-projectOr, better yet, add a new section to "tox.ini":[testenv:coverage]
skip_install = true
deps =
    coverage
commands =
    poetry run pytest --cov=singleton2 ./example-projectIf you like you can add a test-coverage report using thepytest-covlibrary.  I am undecided as to whether this is a good idea.  I feel like using it as a measure of test quality gives a false sence of security.  And, anyway, aiming for 100% coverage is unrealistic.  What is important is testing therelevantbits of code underrealisticuse-cases.To run a coverage report simply run the following command:poetry run pytest --cov=singleton2 ./example-projectThis will measure the coverage of all code under "singleton2" (it focuses on the app and excludes the example-project).  Some of the modules within "singleton2" don't really need to be included in a coverage report.  Therefore we can add the following section to "tox.ini"configuration file ".coveragerc":[run]
omit = singleton2/migrations/*, singleton2/tests/*versioning the codepackaging & publishing the codeAssuming this is arealDjango Reusable App you will want to publish to PyPi.This requires creating an account onpypi.organd creating an API token, then adding that token to your poetry credentials:poetry config pypi-token.pypi <token>Once that is configured, packaging and publishing can be done in a single step:poetry publish --build.continuous integration (workflows)I AM HEREI'll talk about how to configure the documentation below.
I AM HERETODO:distributed tox (just use the -p flag) - doneget tox to build documentation - done but turns out its a bad ideapush documentation to readthedocs - donelook into documentation on github pages (for private repositories)prevent tox from including docs dependencies for tests - done but don't like solutionrun tox on PRs -donehttps://pypi.org/project/tox-gh-actions/dynamic versioning:https://github.com/mtkennerly/poetry-dynamic-versioningbadgestoxTox allows me to test a matrix of package versions.  The obvious thing to do is to test it against different versions of Python & Django.  To speed things up you can run tox in parallel viapoetry run tox -p all(although this doesn't provide as much output).github actionsSo I want to use poetry to run tox which uses pytest as the runner automatically whenever code is pushed to github.  I do this using github actions and the clever library tox-gh-actions as well as github's publically availableInstall Poetry Actionmkdir -p .github/workflowsThe section in tox.ini maps github action matrices to tox environments.  For example in the workflow file:strategy:
    matrix:
      python-version: ["3.8", "3.10"]defines 2 versions of python and the corresponding section in the tox config file:[gh-actions]
python =
    3.8: py38
    3.10: py310maps those to the existing "py38" and "py310" arguments at the top of the tox config file.publishuse dev-py