Guide to packaging, publishing, and deploying DVP CMS plugins.
Development Workflow
Local Development Setup
# 1. Create plugin directory
mkdir my-plugin
cd my-plugin
# 2. Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# 3. Install DVP CMS (development mode)
pip install -e /path/to/dvp-cms
# 4. Install development tools
pip install pytest pytest-cov black mypy
Development Cycle
# 1. Write code
vim plugin.py
# 2. Write tests
vim tests/test_plugin.py
# 3. Run tests
pytest tests/ -v
# 4. Check coverage
pytest --cov=my_plugin tests/
# 5. Format code
black plugin.py
# 6. Type check
mypy plugin.py
# 7. Repeat
Plugin Packaging
Required Files
my-plugin/
├── pyproject.toml # Package configuration (required)
├── README.md # Documentation (required)
├── LICENSE # License file (required)
├── CHANGELOG.md # Version history (recommended)
├── my_plugin/ # Source directory
│ ├── __init__.py
│ ├── plugin.py
│ └── plugin.json
└── tests/ # Test directory
├── __init__.py
└── test_plugin.py
pyproject.toml
Modern Python packaging uses pyproject.toml:
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "dvp-my-plugin"
version = "1.0.0"
description = "My DVP CMS plugin"
readme = "README.md"
requires-python = ">=3.9"
license = {text = "AGPL-3.0"}
authors = [
{name = "Your Name", email = "you@example.com"}
]
keywords = ["dvp-cms", "plugin", "content"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
dependencies = [
"dvp-cms>=0.1.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"pytest-asyncio>=0.21",
"black>=23.0",
"mypy>=1.0",
]
[project.urls]
Homepage = "https://yourplugin.example.com"
Documentation = "https://dvp-my-plugin.readthedocs.io"
[project.entry-points."dvp_cms.plugins"]
my-plugin = "my_plugin:MyPlugin"
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "--cov=my_plugin --cov-report=term-missing"
[tool.black]
line-length = 100
target-version = ['py39']
[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
Entry Points
What are Entry Points?
Entry points allow automatic plugin discovery:
# In pyproject.toml
[project.entry-points."dvp_cms.plugins"]
my-plugin = "my_plugin:MyPlugin"
This tells Python:
- Group:
dvp_cms.plugins - Name:
my-plugin - Location:
my_plugin:MyPlugin(module:class)
How DVP CMS Uses Entry Points
from dvp_cms.plugins import PluginDiscovery
# Auto-discover all installed plugins
discovery = PluginDiscovery()
plugins = discovery.discover_all()
# Each discovered plugin has:
# - name: "my-plugin"
# - plugin_class: MyPlugin
# - metadata: {...}
Local Installation
Development Installation
# Install in editable mode (changes reflect immediately)
cd my-plugin
pip install -e .
# With development dependencies
pip install -e ".[dev]"
Testing Local Installation
# test_installation.py
from my_plugin import MyPlugin
plugin = MyPlugin()
assert plugin.name == "my-plugin"
print("✓ Plugin installed successfully")
python test_installation.py
Uninstalling
pip uninstall dvp-my-plugin
Publishing to PyPI
Prerequisites
- Create PyPI account: https://pypi.org/account/register/
- Install build tools:
pip install build twine
Build Distribution
# Clean previous builds
rm -rf dist/ build/ *.egg-info
# Build source distribution and wheel
python -m build
# Result:
# dist/
# ├── dvp_my_plugin-1.0.0-py3-none-any.whl
# └── dvp-my-plugin-1.0.0.tar.gz
Test on TestPyPI
# Upload to TestPyPI first
twine upload --repository testpypi dist/*
# Test installation from TestPyPI
pip install --index-url https://test.pypi.org/simple/ dvp-my-plugin
Publish to PyPI
# Upload to real PyPI
twine upload dist/*
# Enter credentials when prompted
# Or use token authentication (recommended)
API Token Authentication
# Create token at https://pypi.org/manage/account/token/
# Create ~/.pypirc
cat > ~/.pypirc <<EOF
[pypi]
username = __token__
password = pypi-YOUR_TOKEN_HERE
EOF
chmod 600 ~/.pypirc
Private Repositories
Git Repository Installation
# Install directly from Git (if using version control)
pip install git+https://your-git-server.com/yourusername/dvp-my-plugin.git
# Specific branch
pip install git+https://your-git-server.com/yourusername/dvp-my-plugin.git@develop
# Specific tag
pip install git+https://your-git-server.com/yourusername/dvp-my-plugin.git@v1.0.0
Release Distribution
- Create a release with a version tag
- Build and distribute your package files
- Users can install from the distribution URL
Installation Methods
| Method | Command | Pros | Cons |
|---|---|---|---|
| pip (Recommended) | pip install dvp-my-plugin |
Simple, standard, versioned | Requires package published |
| Git URL | pip install git+https://your-repo... |
No publishing needed | No version pinning |
| Local Path | pip install /path/to/plugin |
Immediate, no network | Not portable |
| requirements.txt | pip install -r requirements.txt |
Batch install | Extra file needed |
Version Management
Semantic Versioning
Follow semver:
MAJOR.MINOR.PATCH
1.0.0 → 1.0.1 (patch: bug fix)
1.0.1 → 1.1.0 (minor: new feature, backward compatible)
1.1.0 → 2.0.0 (major: breaking change)
Version Constraints
In pyproject.toml:
dependencies = [
"dvp-cms>=0.1.0", # Minimum version
"requests>=2.28,<3.0", # Range
"markdown~=3.4", # Compatible release (>= 3.4, < 4.0)
]
Version Bumping
# Manual: Edit pyproject.toml and plugin.py
# Or use bump2version:
pip install bump2version
# Create .bumpversion.cfg
cat > .bumpversion.cfg <<EOF
[bumpversion]
current_version = 1.0.0
commit = True
tag = True
[bumpversion:file:pyproject.toml]
[bumpversion:file:my_plugin/plugin.py]
EOF
# Bump patch version
bump2version patch # 1.0.0 → 1.0.1
# Bump minor version
bump2version minor # 1.0.1 → 1.1.0
# Bump major version
bump2version major # 1.1.0 → 2.0.0
Distribution Best Practices
Pre-Release Checklist
Before publishing:
- All tests passing
- Coverage ≥80%
- README.md complete
- CHANGELOG.md updated
- Version bumped
- License file included
- No hardcoded secrets
- Dependencies pinned
- Entry point configured
- Tested on fresh install
CI/CD Pipeline
Example automation for publishing to PyPI on release:
# Example CI/CD configuration (adapt to your CI system)
name: Publish to PyPI
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
run: git checkout
- name: Set up Python
run: |
python --version
- name: Install dependencies
run: |
pip install build twine
- name: Build package
run: python -m build
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload dist/*
Version Tagging
# Create annotated tag
git tag -a v1.0.0 -m "Release version 1.0.0"
# Push tag to remote
git push origin v1.0.0
# Or push all tags
git push --tags
Quick Reference
Build and Publish
# 1. Update version
vim pyproject.toml # Update version
vim my_plugin/plugin.py # Update version
# 2. Update changelog
vim CHANGELOG.md
# 3. Commit changes
git add .
git commit -m "Release v1.0.0"
# 4. Create tag
git tag -a v1.0.0 -m "Release v1.0.0"
# 5. Build
python -m build
# 6. Test on TestPyPI
twine upload --repository testpypi dist/*
# 7. Test installation
pip install --index-url https://test.pypi.org/simple/ dvp-my-plugin
# 8. Publish to PyPI
twine upload dist/*
# 9. Push to Git
git push origin main --tags
Common Issues
Issue: Entry Point Not Found
Problem: Plugin not discovered after installation
Solution:
- Check
pyproject.tomlhas entry point - Reinstall package:
pip uninstall dvp-my-plugin && pip install dvp-my-plugin
Issue: Import Error
Problem: ModuleNotFoundError: No module named 'my_plugin'
Solution:
- Check package structure has
__init__.py - Verify package name in
pyproject.toml - Ensure installed:
pip list | grep dvp-my-plugin
Issue: Version Conflict
Problem: Dependency conflict during installation
Solution: Relax version constraints in pyproject.toml
dependencies = [
"dvp-cms>=0.1.0", # Instead of ==0.1.0
]
Resources
- Python Packaging: https://packaging.python.org/
- PyPI: https://pypi.org/
- TestPyPI: https://test.pypi.org/
- Semantic Versioning: https://semver.org/
- Keep a Changelog: https://keepachangelog.com/