Skip to content

Commit

Permalink
🚧 Custom sphinx directive to document step
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenbal committed Dec 13, 2024
1 parent 388dc94 commit bf9cd75
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 11 deletions.
Empty file.
104 changes: 104 additions & 0 deletions django_setup_configuration/documentation/model_directive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import yaml
import importlib
from docutils import nodes
from docutils.parsers.rst import Directive
from pydantic import BaseModel
from typing import Type
from django_setup_configuration.fields import DjangoModelRefInfo
from sphinx.directives.code import CodeBlock


def generate_model_example(model: Type[BaseModel]) -> dict:
example_data = {}

# Loop through the annotations of the model to create example data
for field_name, field_type in model.__annotations__.items():
if isinstance(field_type, DjangoModelRefInfo):
# For DjangoModelRef, provide a mock string (model reference)
example_data[field_name] = "mock_django_model_reference"

elif isinstance(field_type, type) and isinstance(field_type, BaseModel):
# For nested Pydantic models, create an example using that model
example_data[field_name] = generate_model_example(field_type)

elif hasattr(field_type, "__origin__") and field_type.__origin__ == list:
# For lists of models or other types, create an example with a list
list_type = field_type.__args__[0] # This is the type inside the list
if isinstance(list_type, type) and issubclass(list_type, BaseModel):
# If the list contains a Pydantic model, generate one example item
example_data[field_name] = [generate_model_example(list_type)]
else:
# Otherwise, just provide a list with a basic example type
example_data[field_name] = [list_type() if list_type else None]

else:
# For simple types (int, str, bool, etc.), generate a basic value
if field_type == str:
example_data[field_name] = "example_string"
elif field_type == int:
example_data[field_name] = 123
elif field_type == bool:
example_data[field_name] = True
elif field_type == float:
example_data[field_name] = 123.45
else:
example_data[field_name] = None # Placeholder for unsupported types

return example_data


# Custom directive for generating a YAML example from a Pydantic model
class PydanticModelExampleDirective(Directive):
has_content = False
required_arguments = 1 # Accept the full import path of the step class as an argument

def run(self):
step_class_path = self.arguments[0]

# Dynamically import the step class
try:
# Split the step class path into module and class name
module_name, class_name = step_class_path.rsplit('.', 1)

# Import the module and get the step class
module = importlib.import_module(module_name)
step_class = getattr(module, class_name)

# Ensure the class has the config_model attribute
if not hasattr(step_class, 'config_model'):
raise ValueError(f"The step class '{step_class}' does not have a 'config_model' attribute.")

config_model = step_class.config_model

# Ensure the config_model is a Pydantic model
if not issubclass(config_model, BaseModel):
raise ValueError(f"The config_model '{config_model}' is not a valid Pydantic model.")

except (ValueError, AttributeError, ImportError) as e:
raise ValueError(f"Step class '{step_class_path}' could not be found or is invalid.") from e

# Derive the `namespace` and `enable_setting` from the step class
namespace = getattr(step_class, 'namespace', None)
enable_setting = getattr(step_class, 'enable_setting', None)

# Generate the model example data
example_data = generate_model_example(config_model)

data = {}
if enable_setting:
data[enable_setting] = True
if namespace:
data[namespace] = example_data

# Convert the example data to YAML format using safe_dump to ensure proper formatting
yaml_example = yaml.safe_dump(data, default_flow_style=False)

# Create a code block with YAML formatting
literal_block = nodes.literal_block(yaml_example, yaml_example)
literal_block['language'] = 'yaml'

# Return the node to be inserted into the document
return [literal_block]

def setup(app):
app.add_directive('pydantic-model-example', PydanticModelExampleDirective)
31 changes: 31 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,43 @@
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import sys
import os
from pathlib import Path

import django

current_dir = Path(__file__).parents[1]
code_directory = current_dir / "django_setup_configuration"

sys.path.insert(0, str(code_directory))

import os
import django
from django.conf import settings

# Mock the Django settings
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mock_settings')

if not settings.configured:
settings.configure(
INSTALLED_APPS=[
'django.contrib.contenttypes', # Required by Django models
'django.contrib.sites', # Required by Django models
'django_setup_configuration', # Required by Django models
# Add minimal apps required by your library
],
DATABASES={
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:', # Use an in-memory database
}
}
)

django.setup()

django.setup()


# -- Project information -----------------------------------------------------

Expand All @@ -37,6 +67,7 @@
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.todo",
"django_setup_configuration.documentation.model_directive",
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
12 changes: 2 additions & 10 deletions docs/sites_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,9 @@ To make use of this, add the step to your ``SETUP_CONFIGURATION_STEPS``:
...
]
Create or update your YAML configuration file with your settings:
Create or update your YAML configuration file with syour settings:

.. code-block:: yaml
sites_config_enable: true
sites_config:
items:
- domain: example.com
name: Example site
- domain: test.example.com
name: Test site
.. pydantic-model-example:: django_setup_configuration.contrib.sites.steps.SitesConfigurationStep

.. note::
The first item in the list will be used to update the current ``Site`` instance,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ classifiers = [
]
requires-python = ">=3.10"
dependencies = [
"django>=3.2",
"django>=3.2,<5.0",
"pydantic>=2",
"pydantic-settings[yaml]>=2.2"
]
Expand Down

0 comments on commit bf9cd75

Please sign in to comment.