Skip to content

Forms

FileOrPathField

FileOrPathField(**kwargs)

Bases: MultiValueField

Form field that accepts either an uploaded file or a typed server-side path.

If a file is uploaded it is saved to a temporary directory and the path to that temp file is returned as the cleaned value (a plain str). If only a path string is typed that string is returned directly.

Typical usage in add_arguments::

from django_admin_runner import FileOrPathField

parser.add_argument("--source", widget=FileOrPathField(), help="CSV path or upload")
Source code in src/django_admin_runner/forms.py
def __init__(self, **kwargs):
    fields = [
        forms.FileField(required=False),
        forms.CharField(required=False),
    ]
    kwargs.setdefault("required", False)
    super().__init__(fields, **kwargs)

FileOrPathWidget

FileOrPathWidget(attrs=None)

Bases: MultiWidget

Renders a file-upload input and a text-path input side by side.

The user can either upload a file or type a path on the server. When ADMIN_RUNNER_UPLOAD_PATH is not set, the file input is hidden and only the text path input is shown.

Source code in src/django_admin_runner/forms.py
def __init__(self, attrs=None):
    super().__init__([forms.FileInput(), forms.TextInput()], attrs)

FileField module-attribute

FileField = FileField

ImageField module-attribute

ImageField = ImageField

form_from_command

form_from_command(command_name: str) -> type[forms.Form]

Build a Django Form class by introspecting command_name's argparse parser.

Both optional (--flag) and positional arguments are included; positional arguments preserve the required=True constraint from argparse.

Filtering rules (applied in order):

  1. Default Django management params are always excluded.
  2. Arguments with hidden=True (custom kwarg on add_argument) are excluded.
  3. exclude_params from the registry entry are excluded.
  4. If params allowlist is set, only those dest names are kept.

Widget / field override priority (highest wins):

  1. widget=<instance> kwarg on add_argument() — specified in the command itself, closest to the argument definition.
  2. widgets dict from the register_command decorator.
  3. Auto-detected from the argparse action type (uses Django admin widgets as the base so the form blends with the rest of the admin interface).

If the value at priority 1 or 2 is a :class:~django.forms.Field instance it replaces the auto-detected field entirely (widget + validation). If it is a :class:~django.forms.Widget instance the auto-detected field is kept but its widget is swapped.

User-specified widgets (priorities 1 and 2) are never overridden by the Unfold auto-replacement. Auto-detected fields (priority 3) receive Unfold widgets when Unfold is installed.

If the registry entry has form_class set, it is returned directly — no auto-generation, no Unfold replacement.

Source code in src/django_admin_runner/forms.py
def form_from_command(command_name: str) -> type[forms.Form]:
    """Build a Django ``Form`` class by introspecting *command_name*'s argparse parser.

    Both optional (``--flag``) and positional arguments are included; positional
    arguments preserve the ``required=True`` constraint from argparse.

    Filtering rules (applied in order):

    1. Default Django management params are always excluded.
    2. Arguments with ``hidden=True`` (custom kwarg on ``add_argument``) are excluded.
    3. ``exclude_params`` from the registry entry are excluded.
    4. If ``params`` allowlist is set, only those ``dest`` names are kept.

    Widget / field override priority (highest wins):

    1. ``widget=<instance>`` kwarg on ``add_argument()`` — specified in the command
       itself, closest to the argument definition.
    2. ``widgets`` dict from the ``register_command`` decorator.
    3. Auto-detected from the argparse action type (uses Django admin widgets as
       the base so the form blends with the rest of the admin interface).

    If the value at priority 1 or 2 is a :class:`~django.forms.Field` instance it
    replaces the auto-detected field entirely (widget + validation).  If it is a
    :class:`~django.forms.Widget` instance the auto-detected field is kept but its
    widget is swapped.

    User-specified widgets (priorities 1 and 2) are **never** overridden by the
    Unfold auto-replacement.  Auto-detected fields (priority 3) receive Unfold
    widgets when Unfold is installed.

    If the registry entry has ``form_class`` set, it is returned directly —
    no auto-generation, no Unfold replacement.
    """
    from django.core.management import get_commands, load_command_class

    from .registry import _registry

    entry = _registry.get(command_name, {})

    # Option C: custom form class — skip all auto-generation
    if form_class := entry.get("form_class"):
        return form_class  # type: ignore[return-value]

    params_allowlist = entry.get("params")
    exclude_set = set(entry.get("exclude_params") or [])
    widgets_override: dict = entry.get("widgets") or {}

    app_name = get_commands().get(command_name, "django.core")

    with _hidden_aware_argparse():
        cmd = load_command_class(app_name, command_name)
        parser = cmd.create_parser("manage.py", command_name)

    fields: dict[str, forms.Field] = {}
    for action in parser._actions:  # noqa: SLF001
        dest = action.dest

        if dest in _DEFAULT_EXCLUDED:
            continue
        if isinstance(action, argparse._HelpAction | argparse._SubParsersAction):  # noqa: SLF001
            continue
        if getattr(action, "hidden", False):
            continue
        if dest in exclude_set:
            continue
        if params_allowlist is not None and dest not in params_allowlist:
            continue

        # Priority: action-level widget > decorator-level widget > auto-detect
        override = getattr(action, "widget", None) or widgets_override.get(dest)
        user_specified = override is not None

        if user_specified:
            if isinstance(override, forms.Field):
                # Full field replacement (e.g. FileOrPathField, FileField, ImageField)
                field: forms.Field | None = override
            else:
                # Widget instance — auto-detect the field, then swap its widget
                field = _action_to_field(action)
                if field is not None:
                    field.widget = override
        else:
            field = _action_to_field(action)

        if field is not None:
            if not user_specified or isinstance(field, FileOrPathField):
                _apply_unfold_widget(field)
            fields[dest] = field

    return type("CommandForm", (forms.Form,), fields)