Skip to content

Metaclass

The ConfigMeta metaclass discovers field definitions on DotEnvConfig subclasses.

metaclass

Metaclass for DotEnvConfig field discovery.

ConfigMeta

Bases: type

Metaclass that discovers field definitions on DotEnvConfig subclasses.

Collects type annotations, associates them with FieldInfo metadata, inherits fields from parent classes, handles Optional defaults, and resolves string annotations when PEP 563 is active.

__new__

__new__(
    mcs,
    name: str,
    bases: tuple[type, ...],
    namespace: dict[str, Any],
) -> type
Source code in dotenvmodel/metaclass.py
def __new__(mcs, name: str, bases: tuple[type, ...], namespace: dict[str, Any]) -> type:
    fields: dict[str, tuple[type, FieldInfo]] = {}
    for base in bases:
        if hasattr(base, "_fields"):
            base_fields = cast("dict[str, tuple[type, FieldInfo]]", base._fields)
            fields.update(base_fields)

    # Eagerly validate a class-level strip_strings setting: a non-bool
    # value is rejected at class-definition time so it can never silently
    # produce char-set stripping (e.g. "true" -> strips t/r/u/e chars) or
    # be treated as truthy (e.g. 1). Only a value present in THIS class's
    # namespace is checked; inherited values were validated at their own
    # definition time.
    if "strip_strings" in namespace and not isinstance(namespace["strip_strings"], bool):
        raise TypeError(
            f"strip_strings must be a bool, got "
            f"{type(namespace['strip_strings']).__name__}: "
            f"{namespace['strip_strings']!r}"
        )

    hints = _get_annotations_from_namespace(namespace)

    for field_name, field_type in hints.items():
        if field_name.startswith("_"):
            continue
        # Class-level settings, not fields
        if field_name in ("env_prefix", "strip_strings"):
            continue

        field_value = namespace.get(field_name, _MISSING)

        if isinstance(field_value, FieldInfo):
            field_info = field_value
            if (
                field_info.default is _MISSING
                and field_info.default_factory is None
                and _is_optional_type(field_type)
            ):
                field_info.default = None
                field_info.required = False
        elif isinstance(field_value, _RequiredSentinel):
            field_info = FieldInfo()
        elif field_value is _MISSING:
            if _is_optional_type(field_type):
                field_info = FieldInfo(default=None)
            else:
                field_info = FieldInfo()
        elif field_value is ...:
            field_info = FieldInfo()
        else:
            field_info = FieldInfo(default=field_value)

        fields[field_name] = (field_type, field_info)
        # Set to None instead of removing so __annotate__ can still resolve
        # the name on Python 3.14+ (PEP 649). FieldInfo objects must not
        # remain as class attributes since they'd be shared across instances.
        if field_name in namespace:
            namespace[field_name] = None

    namespace["_fields"] = fields
    cls = super().__new__(mcs, name, bases, namespace)

    # Resolve string annotations (PEP 563 future import or Python 3.14+ lazy annotations)
    if any(isinstance(ft, str) for ft, _ in cls._fields.values()):
        resolved = _resolve_type_hints(cls)
        if resolved:
            new_fields: dict[str, tuple[type, FieldInfo]] = {}
            for fname, (ftype, finfo) in cls._fields.items():
                rtype = resolved.get(fname, ftype)
                if (
                    finfo.default is _MISSING
                    and finfo.default_factory is None
                    and _is_optional_type(rtype)
                ):
                    finfo.default = None
                    finfo.required = False
                new_fields[fname] = (rtype, finfo)
            cls._fields = new_fields

    return cls