Plugin Development Best Practices

Intermediate Reference Guide

This guide covers best practices for building production-ready DVP CMS plugins.

Code Quality

Type Hints (Required)

Bad:
def process_content(content):
    return content
Good:
def process_content(content: Dict[str, Any]) -> Dict[str, Any]:
    return content

Why: Type hints enable IDE autocomplete, catch errors early, and serve as documentation.

Docstrings (Required)

Use Google-style docstrings:

def dvp_before_content_create(
    self,
    content: Dict[str, Any],
    metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
    """
    Process content before creation.

    Args:
        content: Content data being created
        metadata: Optional metadata about the operation

    Returns:
        Modified content with processing applied

    Raises:
        ValueError: If content validation fails

    Example:
        >>> plugin = MyPlugin()
        >>> result = plugin.dvp_before_content_create({'title': 'Test'})
        >>> print(result['processed'])
        True
    """

Code Organization

class MyPlugin(Plugin, ContentLifecycleHooks):
    """Plugin for..."""

    # 1. Class attributes (metadata)
    name = "my-plugin"
    version = "1.0.0"

    # 2. __init__
    def __init__(self, config: Optional[Dict] = None):
        super().__init__()
        self._validate_config(config)

    # 3. Lifecycle methods
    def initialize(self) -> None:
        super().initialize()

    def shutdown(self) -> None:
        super().shutdown()

    # 4. Public hook implementations
    def dvp_before_content_create(self, content, metadata=None):
        pass

    # 5. Public utility methods
    def get_statistics(self) -> Dict[str, Any]:
        pass

    # 6. Private helper methods (prefix with _)
    def _validate_config(self, config: Optional[Dict]) -> None:
        pass

    def _process_text(self, text: str) -> str:
        pass

Security

Input Validation

Always validate external input:

def dvp_before_content_create(self, content, metadata=None):
    # Validate required fields
    if 'title' not in content:
        raise ValueError("Title is required")

    # Validate types
    if not isinstance(content['title'], str):
        raise TypeError("Title must be a string")

    # Sanitize input
    content['title'] = self._sanitize_html(content['title'])

    return content

SQL Injection Prevention

Never construct SQL directly:
# DANGEROUS!
query = f"SELECT * FROM content WHERE title = '{content['title']}'"
Use parameterized queries:
# Safe
query = "SELECT * FROM content WHERE title = %s"
cursor.execute(query, (content['title'],))

Path Traversal Prevention

Dangerous:
file_path = f"/uploads/{content['filename']}"  # Can be ../../etc/passwd
Safe:
from pathlib import Path

def get_safe_path(self, filename: str) -> Path:
    # Remove path components
    safe_name = Path(filename).name
    base_dir = Path("/uploads")
    full_path = (base_dir / safe_name).resolve()

    # Ensure path is within base_dir
    if not str(full_path).startswith(str(base_dir)):
        raise ValueError("Invalid filename")

    return full_path

Secret Management

Never hardcode secrets:
API_KEY = "sk-1234567890abcdef"  # NO!
Use environment variables:
import os

class MyPlugin(Plugin):
    def __init__(self):
        super().__init__()
        self.api_key = os.getenv('MY_PLUGIN_API_KEY')
        if not self.api_key:
            raise ValueError("MY_PLUGIN_API_KEY environment variable required")

XSS Prevention

import html

def _sanitize_html(self, text: str) -> str:
    """Escape HTML to prevent XSS."""
    return html.escape(text)

Performance

Hook Execution Time

Target: <100ms per hook execution

import time

def dvp_before_content_create(self, content, metadata=None):
    start = time.time()

    # Your processing here
    result = self._process(content)

    elapsed = time.time() - start
    if elapsed > 0.1:  # 100ms
        self.logger.warning(f"Hook execution slow: {elapsed:.3f}s")

    return result

Database Queries

N+1 Query Problem:
for content_id in content_ids:
    content = db.query(Content).filter_by(id=content_id).first()  # Bad!
Batch Queries:
contents = db.query(Content).filter(Content.id.in_(content_ids)).all()

Caching

Cache expensive operations:

from functools import lru_cache

class MyPlugin(Plugin):
    @lru_cache(maxsize=128)
    def _get_keywords(self, text: str) -> List[str]:
        """Cache keyword extraction results."""
        # Expensive operation here
        return self._extract_keywords(text)

Async Operations

Use async for I/O-bound operations:

import asyncio
import aiohttp

async def _fetch_data(self, url: str) -> Dict:
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.json()

Testing

Test Coverage

Minimum: 80% coverage

pytest --cov=my_plugin --cov-report=term-missing

Test Structure

import pytest
from my_plugin import MyPlugin


class TestMyPlugin:
    """Test suite for MyPlugin."""

    def test_plugin_metadata(self):
        """Test plugin has correct metadata."""
        plugin = MyPlugin()
        assert plugin.name == "my-plugin"
        assert plugin.version == "1.0.0"

    def test_hook_implementation(self):
        """Test hook modifies content correctly."""
        plugin = MyPlugin()
        content = {'title': 'Test'}
        result = plugin.dvp_before_content_create(content)
        assert 'processed' in result

    def test_error_handling(self):
        """Test plugin handles errors gracefully."""
        plugin = MyPlugin()
        with pytest.raises(ValueError, match="Title required"):
            plugin.dvp_before_content_create({})

    @pytest.mark.asyncio
    async def test_async_operations(self):
        """Test async hook execution."""
        plugin = MyPlugin()
        result = await plugin.async_operation()
        assert result is not None

Error Handling

Graceful Degradation

def dvp_before_content_create(self, content, metadata=None):
    try:
        # Try to enrich content
        enriched_data = self._fetch_enrichment(content)
        content['enriched'] = enriched_data
    except APIError as e:
        # Log error but don't fail
        self.logger.warning(f"Enrichment failed: {e}")
        content['enriched'] = None
    except Exception as e:
        # Unexpected error - log and continue
        self.logger.error(f"Unexpected error in enrichment: {e}", exc_info=True)

    return content

Helpful Error Messages

Bad:
raise ValueError("Invalid input")
Good:
raise ValueError(
    f"Invalid content: missing required field 'title'. "
    f"Received fields: {list(content.keys())}"
)

Hook Priorities

Priority Ranges

Use semantic priority ranges:

def get_hook_priority(self, hook_name: str) -> int:
    """
    Priority ranges:
    1-10:     Security, authentication (highest)
    10-50:    Validation, sanitization
    50-100:   Content transformation
    100-500:  Enhancement, enrichment
    500-900:  Formatting, presentation
    900-1000: Logging, analytics (lowest)
    """
    if hook_name in ['dvp_before_content_create', 'dvp_before_content_update']:
        return 60  # Transformation
    elif hook_name == 'dvp_content_created':
        return 950  # Logging
    return 500  # Default

Configuration

Configuration Schema

from typing import Optional, Dict, Any
from dataclasses import dataclass

@dataclass
class PluginConfig:
    """Configuration for MyPlugin."""
    api_endpoint: str
    api_key: str
    timeout: int = 30
    retry_count: int = 3
    cache_enabled: bool = True

class MyPlugin(Plugin):
    def __init__(self, config: Optional[Dict[str, Any]] = None):
        super().__init__()
        self.config = self._parse_config(config or {})

    def _parse_config(self, config: Dict[str, Any]) -> PluginConfig:
        """Parse and validate configuration."""
        try:
            return PluginConfig(**config)
        except TypeError as e:
            raise ValueError(f"Invalid plugin configuration: {e}")

Versioning

Semantic Versioning

Follow semver:

class MyPlugin(Plugin):
    name = "my-plugin"
    version = "1.2.3"  # MAJOR.MINOR.PATCH

Deployment

Package Structure

my-plugin/
├── plugin.py           # Main code
├── plugin.json         # Metadata
├── __init__.py         # Package init
├── README.md           # Documentation
├── CHANGELOG.md        # Version history
├── LICENSE             # License file
├── tests/              # Test suite
│   ├── __init__.py
│   └── test_plugin.py
├── pyproject.toml      # Python package config
└── .gitignore          # Git ignore rules

pyproject.toml

[project]
name = "dvp-my-plugin"
version = "1.0.0"
description = "My DVP CMS plugin"
authors = [{name = "Your Name", email = "you@example.com"}]
license = {text = "AGPL-3.0"}
requires-python = ">=3.9"
dependencies = [
    "dvp-cms>=0.1.0",
]

[project.entry-points."dvp_cms.plugins"]
my-plugin = "my_plugin:MyPlugin"

[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"

Checklist: Production-Ready Plugin

Before releasing, verify:

Code Quality:

  • Type hints on all public methods
  • Docstrings (Google style)
  • No hardcoded values (use config)
  • Follows PEP 8 style guide

Security:

  • Input validation on all external data
  • No SQL injection vulnerabilities
  • No path traversal vulnerabilities
  • Secrets in environment variables

Performance:

  • Hook execution <100ms
  • Database queries optimized
  • Expensive operations cached
  • Async for I/O operations

Testing:

  • Test coverage ≥80%
  • All tests passing
  • Integration tests included
  • Edge cases tested

Documentation:

  • README.md complete
  • Usage examples provided
  • Configuration documented
  • CHANGELOG.md maintained

Deployment:

  • plugin.json complete and valid
  • pyproject.toml configured
  • Entry point defined
  • License file included

Resources