Creating reusable Django packages allows you to share functionality across projects and contribute to the Django ecosystem. This comprehensive guide covers package design, development best practices, testing strategies, and distribution methods for building high-quality Django packages.
django-mypackage/
├── setup.py # Package configuration
├── setup.cfg # Additional setup configuration
├── pyproject.toml # Modern Python packaging
├── MANIFEST.in # Include additional files
├── README.rst # Package documentation
├── LICENSE # License file
├── CHANGELOG.md # Version history
├── requirements/ # Requirements files
│ ├── base.txt
│ ├── development.txt
│ └── testing.txt
├── docs/ # Documentation
│ ├── conf.py
│ ├── index.rst
│ └── ...
├── tests/ # Test suite
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_models.py
│ ├── test_views.py
│ └── ...
├── mypackage/ # Main package
│ ├── __init__.py
│ ├── apps.py # Django app configuration
│ ├── models.py # Models
│ ├── views.py # Views
│ ├── admin.py # Admin configuration
│ ├── urls.py # URL patterns
│ ├── forms.py # Forms
│ ├── managers.py # Custom managers
│ ├── signals.py # Signal handlers
│ ├── utils.py # Utility functions
│ ├── exceptions.py # Custom exceptions
│ ├── settings.py # Package settings
│ ├── migrations/ # Database migrations
│ ├── templates/ # Templates
│ │ └── mypackage/
│ ├── static/ # Static files
│ │ └── mypackage/
│ └── locale/ # Translations
└── example_project/ # Example Django project
├── manage.py
├── settings.py
└── ...
# setup.py
import os
from setuptools import setup, find_packages
# Read long description from README
with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
README = readme.read()
# Read requirements
def read_requirements(filename):
with open(os.path.join('requirements', filename)) as f:
return [line.strip() for line in f if line.strip() and not line.startswith('#')]
setup(
name='django-mypackage',
version='1.0.0',
description='A reusable Django package for awesome functionality',
long_description=README,
long_description_content_type='text/x-rst',
author='Your Name',
author_email='your.email@example.com',
url='https://github.com/yourusername/django-mypackage',
license='MIT',
packages=find_packages(exclude=['tests*', 'example_project*']),
include_package_data=True,
zip_safe=False,
install_requires=read_requirements('base.txt'),
extras_require={
'dev': read_requirements('development.txt'),
'test': read_requirements('testing.txt'),
},
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Framework :: Django',
'Framework :: Django :: 3.2',
'Framework :: Django :: 4.0',
'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
],
python_requires='>=3.8',
keywords='django, package, reusable',
project_urls={
'Documentation': 'https://django-mypackage.readthedocs.io/',
'Source': 'https://github.com/yourusername/django-mypackage',
'Tracker': 'https://github.com/yourusername/django-mypackage/issues',
},
)
# pyproject.toml (modern approach)
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "django-mypackage"
description = "A reusable Django package for awesome functionality"
readme = "README.rst"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "your.email@example.com"},
]
keywords = ["django", "package", "reusable"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.0",
"Framework :: Django :: 4.1",
"Framework :: Django :: 4.2",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
dependencies = [
"Django>=3.2",
]
dynamic = ["version"]
[project.optional-dependencies]
dev = [
"black",
"flake8",
"isort",
"mypy",
"pre-commit",
]
test = [
"pytest",
"pytest-django",
"pytest-cov",
"factory-boy",
]
docs = [
"sphinx",
"sphinx-rtd-theme",
]
[project.urls]
Homepage = "https://github.com/yourusername/django-mypackage"
Documentation = "https://django-mypackage.readthedocs.io/"
Repository = "https://github.com/yourusername/django-mypackage"
"Bug Tracker" = "https://github.com/yourusername/django-mypackage/issues"
[tool.setuptools_scm]
write_to = "mypackage/_version.py"
# MANIFEST.in
include README.rst
include LICENSE
include CHANGELOG.md
recursive-include mypackage/templates *
recursive-include mypackage/static *
recursive-include mypackage/locale *
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
# mypackage/apps.py
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class MyPackageConfig(AppConfig):
"""Django app configuration for MyPackage"""
default_auto_field = 'django.db.models.BigAutoField'
name = 'mypackage'
verbose_name = _('My Package')
def ready(self):
"""Initialize package when Django starts"""
# Import signal handlers
from . import signals # noqa
# Perform package initialization
self.initialize_package()
def initialize_package(self):
"""Initialize package settings and configuration"""
from .settings import get_setting
# Validate required settings
required_settings = ['MYPACKAGE_API_KEY', 'MYPACKAGE_SECRET']
for setting in required_settings:
if not get_setting(setting):
import warnings
warnings.warn(
f"MyPackage: {setting} is not configured. "
f"Some features may not work correctly.",
UserWarning
)
# Initialize external services
self.initialize_external_services()
def initialize_external_services(self):
"""Initialize external service connections"""
from .services import ExternalAPIService
try:
ExternalAPIService.initialize()
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.warning(f"Failed to initialize external services: {e}")
# mypackage/__init__.py
default_app_config = 'mypackage.apps.MyPackageConfig'
# Version information
try:
from ._version import version as __version__
except ImportError:
__version__ = 'unknown'
# Package metadata
__title__ = 'django-mypackage'
__description__ = 'A reusable Django package for awesome functionality'
__author__ = 'Your Name'
__email__ = 'your.email@example.com'
__license__ = 'MIT'
__copyright__ = 'Copyright 2023 Your Name'
# Public API
from .models import MyModel
from .forms import MyForm
from .views import MyView
from .utils import my_utility_function
__all__ = [
'MyModel',
'MyForm',
'MyView',
'my_utility_function',
]
# mypackage/settings.py
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
# Default settings
DEFAULTS = {
'MYPACKAGE_API_KEY': None,
'MYPACKAGE_SECRET': None,
'MYPACKAGE_TIMEOUT': 30,
'MYPACKAGE_CACHE_TIMEOUT': 3600,
'MYPACKAGE_DEBUG': False,
'MYPACKAGE_ALLOWED_HOSTS': [],
'MYPACKAGE_RATE_LIMIT': 100,
'MYPACKAGE_FEATURES': {
'feature_a': True,
'feature_b': False,
'feature_c': True,
},
}
def get_setting(name, default=None):
"""Get package setting with fallback to default"""
# First check Django settings
if hasattr(settings, name):
return getattr(settings, name)
# Then check package defaults
if name in DEFAULTS:
return DEFAULTS[name]
# Finally use provided default
return default
def require_setting(name):
"""Get required setting or raise error"""
value = get_setting(name)
if value is None:
raise ImproperlyConfigured(
f"MyPackage requires {name} to be configured in Django settings"
)
return value
class PackageSettings:
"""Settings class for package configuration"""
def __init__(self):
self._settings = {}
self._load_settings()
def _load_settings(self):
"""Load all package settings"""
for key in DEFAULTS:
self._settings[key] = get_setting(key)
def __getattr__(self, name):
"""Get setting value"""
setting_name = f'MYPACKAGE_{name.upper()}'
if setting_name in self._settings:
return self._settings[setting_name]
raise AttributeError(f"'{self.__class__.__name__}' has no setting '{name}'")
def is_feature_enabled(self, feature_name):
"""Check if feature is enabled"""
features = self._settings.get('MYPACKAGE_FEATURES', {})
return features.get(feature_name, False)
def reload(self):
"""Reload settings from Django configuration"""
self._load_settings()
# Global settings instance
package_settings = PackageSettings()
# Convenience functions
def is_debug_enabled():
"""Check if debug mode is enabled"""
return package_settings.debug
def get_api_key():
"""Get API key (required)"""
return require_setting('MYPACKAGE_API_KEY')
def get_cache_timeout():
"""Get cache timeout"""
return package_settings.cache_timeout
# mypackage/models.py
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.utils.translation import gettext_lazy as _
from .managers import MyModelManager
from .settings import package_settings
class AbstractBaseModel(models.Model):
"""Abstract base model for package models"""
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at'))
class Meta:
abstract = True
class MyModel(AbstractBaseModel):
"""Main model for the package"""
name = models.CharField(max_length=200, verbose_name=_('Name'))
description = models.TextField(blank=True, verbose_name=_('Description'))
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
# Generic foreign key for flexibility
content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
null=True,
blank=True
)
object_id = models.PositiveIntegerField(null=True, blank=True)
content_object = GenericForeignKey('content_type', 'object_id')
# Custom manager
objects = MyModelManager()
class Meta:
verbose_name = _('My Model')
verbose_name_plural = _('My Models')
ordering = ['-created_at']
indexes = [
models.Index(fields=['content_type', 'object_id']),
models.Index(fields=['is_active', 'created_at']),
]
def __str__(self):
return self.name
def get_absolute_url(self):
"""Get absolute URL for this model"""
from django.urls import reverse
return reverse('mypackage:detail', kwargs={'pk': self.pk})
def is_feature_enabled(self, feature_name):
"""Check if feature is enabled for this instance"""
return package_settings.is_feature_enabled(feature_name)
# mypackage/managers.py
from django.db import models
from django.db.models import Q
class MyModelQuerySet(models.QuerySet):
"""Custom queryset for MyModel"""
def active(self):
"""Filter active instances"""
return self.filter(is_active=True)
def for_content_object(self, obj):
"""Filter by content object"""
from django.contrib.contenttypes.models import ContentType
content_type = ContentType.objects.get_for_model(obj)
return self.filter(content_type=content_type, object_id=obj.pk)
def search(self, query):
"""Search by name and description"""
return self.filter(
Q(name__icontains=query) | Q(description__icontains=query)
)
class MyModelManager(models.Manager):
"""Custom manager for MyModel"""
def get_queryset(self):
return MyModelQuerySet(self.model, using=self._db)
def active(self):
return self.get_queryset().active()
def for_content_object(self, obj):
return self.get_queryset().for_content_object(obj)
def search(self, query):
return self.get_queryset().search(query)
# mypackage/views.py
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from .models import MyModel
from .forms import MyModelForm
from .mixins import PackageMixin
class MyModelListView(PackageMixin, ListView):
"""List view for MyModel"""
model = MyModel
template_name = 'mypackage/mymodel_list.html'
context_object_name = 'objects'
paginate_by = 20
def get_queryset(self):
"""Get filtered queryset"""
queryset = super().get_queryset().active()
# Apply search filter
search_query = self.request.GET.get('q')
if search_query:
queryset = queryset.search(search_query)
return queryset
def get_context_data(self, **kwargs):
"""Add extra context"""
context = super().get_context_data(**kwargs)
context['search_query'] = self.request.GET.get('q', '')
return context
class MyModelDetailView(PackageMixin, DetailView):
"""Detail view for MyModel"""
model = MyModel
template_name = 'mypackage/mymodel_detail.html'
context_object_name = 'object'
def get_queryset(self):
return super().get_queryset().active()
class MyModelCreateView(PackageMixin, LoginRequiredMixin, CreateView):
"""Create view for MyModel"""
model = MyModel
form_class = MyModelForm
template_name = 'mypackage/mymodel_form.html'
def form_valid(self, form):
"""Handle valid form submission"""
# Add custom logic before saving
response = super().form_valid(form)
# Send signal or perform additional actions
from .signals import mymodel_created
mymodel_created.send(sender=self.model, instance=self.object, request=self.request)
return response
# API views
@method_decorator(csrf_exempt, name='dispatch')
class MyModelAPIView(PackageMixin, ListView):
"""API view for MyModel"""
model = MyModel
def get(self, request, *args, **kwargs):
"""Handle GET request"""
queryset = self.get_queryset().active()
# Apply filters
search_query = request.GET.get('q')
if search_query:
queryset = queryset.search(search_query)
# Serialize data
data = [
{
'id': obj.id,
'name': obj.name,
'description': obj.description,
'created_at': obj.created_at.isoformat(),
}
for obj in queryset[:100] # Limit results
]
return JsonResponse({'results': data})
# mypackage/mixins.py
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
class PackageMixin:
"""Base mixin for package views"""
def dispatch(self, request, *args, **kwargs):
"""Add package-specific logic to dispatch"""
# Check if package is properly configured
from .settings import package_settings
if not package_settings.api_key and package_settings.debug:
messages.warning(
request,
_('MyPackage is not fully configured. Check your settings.')
)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
"""Add package context"""
context = super().get_context_data(**kwargs)
context['package_version'] = self.get_package_version()
context['package_settings'] = self.get_package_settings()
return context
def get_package_version(self):
"""Get package version"""
from . import __version__
return __version__
def get_package_settings(self):
"""Get package settings for templates"""
from .settings import package_settings
return {
'debug': package_settings.debug,
'features': package_settings._settings.get('MYPACKAGE_FEATURES', {}),
}
# mypackage/forms.py
from django import forms
from django.utils.translation import gettext_lazy as _
from .models import MyModel
class MyModelForm(forms.ModelForm):
"""Form for MyModel"""
class Meta:
model = MyModel
fields = ['name', 'description', 'is_active']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add custom validation or field modifications
self.fields['name'].help_text = _('Enter a unique name for this item')
# Make fields required based on settings
from .settings import package_settings
if package_settings.is_feature_enabled('require_description'):
self.fields['description'].required = True
def clean_name(self):
"""Custom validation for name field"""
name = self.cleaned_data['name']
# Check for uniqueness (excluding current instance)
queryset = MyModel.objects.filter(name=name)
if self.instance.pk:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
raise forms.ValidationError(_('A model with this name already exists.'))
return name
# tests/conftest.py
import pytest
from django.test import TestCase
from django.contrib.auth.models import User
from mypackage.models import MyModel
@pytest.fixture
def user():
"""Create test user"""
return User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
@pytest.fixture
def mymodel():
"""Create test MyModel instance"""
return MyModel.objects.create(
name='Test Model',
description='Test description',
is_active=True
)
# tests/test_models.py
import pytest
from django.test import TestCase
from django.contrib.contenttypes.models import ContentType
from mypackage.models import MyModel
class MyModelTestCase(TestCase):
"""Test cases for MyModel"""
def setUp(self):
self.model = MyModel.objects.create(
name='Test Model',
description='Test description'
)
def test_string_representation(self):
"""Test string representation"""
self.assertEqual(str(self.model), 'Test Model')
def test_absolute_url(self):
"""Test get_absolute_url method"""
url = self.model.get_absolute_url()
self.assertEqual(url, f'/mypackage/{self.model.pk}/')
def test_manager_methods(self):
"""Test custom manager methods"""
# Test active method
active_models = MyModel.objects.active()
self.assertIn(self.model, active_models)
# Test inactive model
self.model.is_active = False
self.model.save()
active_models = MyModel.objects.active()
self.assertNotIn(self.model, active_models)
def test_search_functionality(self):
"""Test search functionality"""
# Create additional models
MyModel.objects.create(name='Another Model', description='Different description')
MyModel.objects.create(name='Third Model', description='Test content')
# Test search by name
results = MyModel.objects.search('Test')
self.assertEqual(results.count(), 2)
# Test search by description
results = MyModel.objects.search('Different')
self.assertEqual(results.count(), 1)
# tests/test_views.py
import pytest
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from mypackage.models import MyModel
class MyModelViewTestCase(TestCase):
"""Test cases for MyModel views"""
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.model = MyModel.objects.create(
name='Test Model',
description='Test description'
)
def test_list_view(self):
"""Test list view"""
url = reverse('mypackage:list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test Model')
def test_detail_view(self):
"""Test detail view"""
url = reverse('mypackage:detail', kwargs={'pk': self.model.pk})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.model.name)
def test_create_view_requires_login(self):
"""Test create view requires authentication"""
url = reverse('mypackage:create')
response = self.client.get(url)
# Should redirect to login
self.assertEqual(response.status_code, 302)
def test_create_view_authenticated(self):
"""Test create view with authentication"""
self.client.login(username='testuser', password='testpass123')
url = reverse('mypackage:create')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_api_view(self):
"""Test API view"""
url = reverse('mypackage:api')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
data = response.json()
self.assertIn('results', data)
self.assertEqual(len(data['results']), 1)
# tests/test_forms.py
from django.test import TestCase
from mypackage.forms import MyModelForm
from mypackage.models import MyModel
class MyModelFormTestCase(TestCase):
"""Test cases for MyModelForm"""
def test_valid_form(self):
"""Test valid form submission"""
form_data = {
'name': 'Test Model',
'description': 'Test description',
'is_active': True
}
form = MyModelForm(data=form_data)
self.assertTrue(form.is_valid())
def test_duplicate_name_validation(self):
"""Test duplicate name validation"""
# Create existing model
MyModel.objects.create(name='Existing Model')
form_data = {
'name': 'Existing Model',
'description': 'Test description',
'is_active': True
}
form = MyModelForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('name', form.errors)
Building reusable Django packages requires careful attention to design, documentation, testing, and distribution. Focus on creating flexible, well-documented packages that solve real problems and integrate seamlessly with Django's ecosystem. Proper testing, versioning, and community engagement are essential for successful package adoption and maintenance.
Working with Signals
Django signals provide a decoupled way to allow certain senders to notify a set of receivers when actions occur. This comprehensive guide covers advanced signal patterns, performance considerations, and best practices for building event-driven Django applications.
Integrating Microservices
Microservices architecture breaks down monolithic applications into smaller, independent services that communicate over well-defined APIs. This guide covers strategies for integrating Django applications with microservices, including service communication patterns, data consistency, and deployment considerations.