Source code for cfx.types.typed_field

"""Annotation-native field factory.

``Field()`` is the primary way to declare config fields when you want the
field type inferred automatically from the type annotation::

    class SearchConfig(Config):
        n_sigma: float = Field(5.0, "Detection threshold", minval=0.0)
        method: Literal["DBSCAN", "RANSAC"] = Field("DBSCAN", "Algorithm")

For custom field types or advanced validation, import explicit types from
``cfx.types`` and declare them directly::

    from cfx.types import Float, Options

    class SearchConfig(Config):
        n_sigma = Float(5.0, "Detection threshold", minval=0.0)
        method  = Options(("DBSCAN", "RANSAC"), "Algorithm")
"""

import datetime
import pathlib
import types  # UnionType, support for int|float expressions in Py<3.14
import typing as t

from .types import (
    Any,
    Bool,
    Date,
    DateTime,
    Dict,
    Float,
    Int,
    List,
    MultiOptions,
    Options,
    Path,
    Range,
    Scalar,
    Seed,
    String,
    Time,
)

__all__ = ["Field", "FieldSpec", "resolve_field_spec"]


[docs] class FieldSpec: """Placeholder returned by ``Field()``; resolved at class-definition time. Users should not instantiate this directly — use ``Field()`` instead. ``Config.__init_subclass__`` replaces every ``FieldSpec`` with the appropriate ``ConfigField`` subclass before any instance is created. """ def __init__(self, default, doc, **kwargs): self.default = default self.doc = doc self.kwargs = kwargs
[docs] def Field(default, doc="", **kwargs): """Declare an annotation-native config field. Use as the right-hand side of a type-annotated class attribute on a ``Config`` subclass. The concrete ``ConfigField`` subclass is inferred from the annotation at class-definition time:: from typing import Literal from cfx import Config, Field class SearchConfig(Config): n_sigma: float = Field(5.0, "Detection threshold", minval=0.0) method: Literal["DBSCAN", "RANSAC"] = Field("DBSCAN", "Algorithm") verbose: bool = Field(False, "Enable verbose output") Callable defaults are supported for computed fields:: class DerivedConfig(Config): base: float = Field(1.0, "Base value") derived: float = Field(lambda self: self.base * 2, "Derived value") Parameters ---------- default : `object` or `callable` Default value or lazy factory accepting the owning instance. doc : `str`, optional Human-readable description shown in display tables. **kwargs Any keyword argument accepted by the resolved field type (e.g. ``minval=``, ``maxval=``, ``env=``, ``static=``, ``transient=``). Returns ------- spec : `FieldSpec` Placeholder resolved to a ``ConfigField`` at class-definition time. """ return FieldSpec(default, doc, **kwargs)
[docs] def resolve_field_spec(name, spec, annotation): """Instantiate the right ``ConfigField`` subclass for an annotation. Called by ``Config.__init_subclass__`` for every ``FieldSpec`` in the class body. Not part of the end-user API but importable for advanced use. Parameters ---------- name : `str` The attribute name (used in error messages). spec : `FieldSpec` The placeholder carrying default, doc, and extra kwargs. annotation : `type` The resolved type annotation for this attribute. Returns ------- field : `ConfigField` A fully constructed field descriptor. Raises ------ TypeError If the annotation cannot be mapped to a known ``ConfigField`` type. """ origin = t.get_origin(annotation) args = t.get_args(annotation) # Final[T] → static=True, unwrap T static = spec.kwargs.pop("static", False) if origin is t.Final: static = True annotation = args[0] if args else t.Any origin = t.get_origin(annotation) args = t.get_args(annotation) kw = {"static": static, **spec.kwargs} if static else dict(spec.kwargs) # Literal[...] → Options (Options takes options first, not default) if origin is t.Literal: return Options(args, spec.doc, default_value=spec.default, **kw) # set[Literal[...]] → MultiOptions if origin is set and args and t.get_origin(args[0]) is t.Literal: return MultiOptions( t.get_args(args[0]), spec.doc, default_value=spec.default, **kw ) # list[T] → List if origin is list: elem = args[0] if args else None return List(spec.default, spec.doc, element_type=elem, **kw) # tuple[T, T] → Range if origin is tuple and len(args) == 2: return Range(spec.default, spec.doc, **kw) # Union types if origin is t.Union or origin is types.UnionType: non_none = [a for a in args if a is not type(None)] if len(non_none) == 1 and type(None) in args and non_none[0] is int: return Seed(spec.default, spec.doc, **kw) if set(non_none) == {int, float} and type(None) not in args: return Scalar(spec.default, spec.doc, **kw) # 1-to-1 map — t.Any avoids shadowing cfx.types.Any defined above type_map = { bool: Bool, int: Int, float: Float, str: String, pathlib.Path: Path, datetime.date: Date, datetime.time: Time, datetime.datetime: DateTime, dict: Dict, t.Any: Any, } if annotation in type_map: return type_map[annotation](spec.default, spec.doc, **kw) raise TypeError( f"Cannot infer field type from annotation {annotation!r} " f"for {name!r}. Declare the field explicitly using a type from " "cfx.types (e.g. Float, Int, Options) or subclass ConfigField." )