Skip to content

props

props #

check_prop — a component prop checked against its annotation when the component is built.

  1. A primitive annotation (bool, int, float, str) admits exactly its Python type (fits_primitive); a Literal admits exactly the values it lists; JSONValue admits anything JSON-native (json_native); object admits anything.
  2. A union keeps the first member that fits (first_fitting); a list, tuple or dict checks each item against its type argument (type_argument), leaving a Missing item out; a cg scalar is coerced by its own coerce; a container, struct, component or any other class is accepted by instance (a dict or list becomes the annotated Map or Array).
  3. Every failure names the prop's position and the annotation (annotation_name), so a mistake is found by name, not by guessing.

PRIMITIVES = (bool, int, float, str) module-attribute #

The annotations a value must match by Python type.

annotation_name(annotation) #

annotation as it reads in an error message.

assert annotation_name(str) == "str"
assert annotation_name(str | None) == "str | None"
Source code in capturegraph-lib/capturegraph/ui/props.py
def annotation_name(annotation: object) -> str:
    """``annotation`` as it reads in an error message.

    ```python
    assert annotation_name(str) == "str"
    assert annotation_name(str | None) == "str | None"
    ```
    """
    return getattr(annotation, "__name__", None) or repr(annotation)

check_prop(value, annotation, position) #

value as the prop annotation admits it at position, coerced where a scalar is.

assert check_prop([1, 2], list[int], "Stat.values") == [1, 2]
assert check_prop("photo.jpeg", cg.Image, "ImageViewer.image") == cg.Image("photo.jpeg")

Raises:

Type Description
TypeError

If value does not fit annotation; the message names position.

Source code in capturegraph-lib/capturegraph/ui/props.py
def check_prop(value: object, annotation: object, position: str) -> object:
    """``value`` as the prop ``annotation`` admits it at ``position``, coerced where a scalar is.

    ```python
    assert check_prop([1, 2], list[int], "Stat.values") == [1, 2]
    assert check_prop("photo.jpeg", cg.Image, "ImageViewer.image") == cg.Image("photo.jpeg")
    ```

    Raises:
        TypeError: If ``value`` does not fit ``annotation``; the message names ``position``.
    """
    if annotation is JSONValue:
        return json_native(value, position)
    if annotation is object:
        return value
    if annotation is type(None):
        if value is None:
            return None
        raise TypeError(f"{position}: expected None, got {value!r}")
    if annotation in PRIMITIVES:
        if fits_primitive(value, annotation):
            return value
        raise TypeError(f"{position}: expected {annotation_name(annotation)}, got {value!r}")
    origin = get_origin(annotation)
    if origin is Literal:
        if value in get_args(annotation):
            return value
        raise TypeError(f"{position}: expected one of {get_args(annotation)}, got {value!r}")
    if origin is UnionType or origin is Union:
        return first_fitting(
            annotation,
            position,
            lambda member: check_prop(value, member, position),
        )
    if origin in (list, tuple) or annotation in (list, tuple):
        if isinstance(value, (list, tuple)):
            element = type_argument(annotation, 0)
            items = [
                check_prop(item, element, f"{position}.{index}")
                for index, item in enumerate(value)
                if not isinstance(item, _Missing)
            ]
            return tuple(items) if (origin or annotation) is tuple else items
    elif origin is dict or annotation is dict:
        if isinstance(value, dict):
            element = type_argument(annotation, 1)
            return {
                text_key(key, position): check_prop(item, element, f"{position}.{key}")
                for key, item in value.items()
            }
    elif isinstance(annotation, type):
        return _check_instance(value, annotation, position)
    raise TypeError(f"{position}: cannot read {value!r} as {annotation_name(annotation)}")

first_fitting(annotation, position, attempt) #

attempt on each member of the union annotation, returning the first that fits.

first_fitting(float | str, "Stat.value", lambda member: check_prop(value, member, position))

Raises:

Type Description
TypeError

If no member fits; the message lists each member's reason.

Source code in capturegraph-lib/capturegraph/ui/props.py
def first_fitting[T](annotation: object, position: str, attempt: Callable[[object], T]) -> T:
    """``attempt`` on each member of the union ``annotation``, returning the first that fits.

    ```python
    first_fitting(float | str, "Stat.value", lambda member: check_prop(value, member, position))
    ```

    Raises:
        TypeError: If no member fits; the message lists each member's reason.
    """
    reasons: list[str] = []
    for member in get_args(annotation):
        try:
            return attempt(member)
        except (TypeError, ValueError) as error:
            reasons.append(f"{annotation_name(member)}: {error}")
    raise TypeError(
        f"{position}: no alternative of {annotation_name(annotation)} fits ({'; '.join(reasons)})"
    )

fits_primitive(value, annotation) #

Whether value is exactly the primitive annotation names (bool is not int).

assert fits_primitive(2.5, float) and fits_primitive(2, float)
assert not fits_primitive(True, int)
Source code in capturegraph-lib/capturegraph/ui/props.py
def fits_primitive(value: object, annotation: object) -> bool:
    """Whether ``value`` is exactly the primitive ``annotation`` names (``bool`` is not ``int``).

    ```python
    assert fits_primitive(2.5, float) and fits_primitive(2, float)
    assert not fits_primitive(True, int)
    ```
    """
    if annotation is bool:
        return type(value) is bool
    if annotation is int:
        return type(value) is int
    if annotation is float:
        return type(value) is not bool and isinstance(value, (int, float))
    return isinstance(value, str)

json_native(value, position) #

value unchanged if it is JSON-native through and through.

assert json_native({"a": [1, 2.5, None]}, "Chart.spec") == {"a": [1, 2.5, None]}

Raises:

Type Description
TypeError

If a nested value is not JSON-native, or an object key is not a string.

Source code in capturegraph-lib/capturegraph/ui/props.py
def json_native(value: object, position: str) -> JSONValue:
    """``value`` unchanged if it is JSON-native through and through.

    ```python
    assert json_native({"a": [1, 2.5, None]}, "Chart.spec") == {"a": [1, 2.5, None]}
    ```

    Raises:
        TypeError: If a nested value is not JSON-native, or an object key is not a string.
    """
    if value is None or isinstance(value, (bool, int, float, str)):
        return value
    if isinstance(value, (list, tuple)):
        return [json_native(item, f"{position}.{index}") for index, item in enumerate(value)]
    if isinstance(value, dict):
        return {
            text_key(key, position): json_native(item, f"{position}.{key}")
            for key, item in value.items()
        }
    raise TypeError(f"{position}: {value!r} is not JSON-native")

text_key(key, position) #

key as a dict prop's key.

assert text_key("front", "Gallery.images") == "front"

Raises:

Type Description
TypeError

If key is not a string.

Source code in capturegraph-lib/capturegraph/ui/props.py
def text_key(key: object, position: str) -> str:
    """``key`` as a dict prop's key.

    ```python
    assert text_key("front", "Gallery.images") == "front"
    ```

    Raises:
        TypeError: If ``key`` is not a string.
    """
    if isinstance(key, str):
        return key
    raise TypeError(f"{position}: dict keys are strings, got {key!r}")

type_argument(annotation, index) #

The index-th type argument of a generic alias, object when unparameterized.

assert type_argument(list[str], 0) is str
assert type_argument(dict, 1) is object
Source code in capturegraph-lib/capturegraph/ui/props.py
def type_argument(annotation: object, index: int) -> object:
    """The ``index``-th type argument of a generic alias, ``object`` when unparameterized.

    ```python
    assert type_argument(list[str], 0) is str
    assert type_argument(dict, 1) is object
    ```
    """
    arguments = get_args(annotation)
    return arguments[index] if len(arguments) > index else object