Skip to content

kumiki.kiwari

Flat import path

kumiki/__init__.py re-exports everything on this page via from kumiki import *, so every name below is also available directly as kumiki.NOTHING -- you do not need to import from the submodule path shown in the heading above.

kumiki.kiwari

Kiwari -- the numbers a frame is built from, and what they are allowed to be.

Kiwari (木割) is the traditional system that sets every member's dimension from a small set of base numbers. That is what this is: a frame declares the handful of numbers it is proportioned from, and kigumi puts a control on each.

A builder declares its kiwari in its own body and hands it back on the frame::

def build_frame(k: Kiwari | None = None) -> Frame:
    k = kiwari(
        legs=kiwari.count(4, minimum=3, about="Number of legs"),
        seat_height=kiwari.length(mm(450)),
        butt_end=kiwari.choice(TimberEnd, TimberEnd.TOP),
    ).resolve(k)

    for i in range(k.count("legs")):
        ...
    return Frame.from_joints(joints, kiwari=k)

Nothing here is global and nothing is mutated: kiwari(...) makes a declaration whose values are its defaults, resolve returns a new one with incoming values laid over the top, and Frame carries the result. Calling build_frame() bare gets the defaults, which is what a script or a test wants.

Declaring and binding are one type on purpose. Values are never useful without the declarations -- you cannot validate, coerce, or fall back to a default without them -- so splitting them would mean every caller carried both and something had to check they agreed. Here there is only ever one schema in the room, and an accessor asking for the wrong thing says so at the call site::

k.length("legs")   # TypeError: 'legs' is a count, not a length

Reading a value back out is typed, because a length and a count are both floats once they are numbers, and the declaration is the only thing that knows which one a key is.

NOTHING module-attribute

NOTHING = _Nothing()

LENGTH module-attribute

LENGTH = 'length'

ANGLE module-attribute

ANGLE = 'angle'

COUNT module-attribute

COUNT = 'count'

NUMBER module-attribute

NUMBER = 'number'

FLAG module-attribute

FLAG = 'flag'

TEXT module-attribute

TEXT = 'text'

CHOICE module-attribute

CHOICE = 'choice'

POINT2 module-attribute

POINT2 = 'point2'

POINT3 module-attribute

POINT3 = 'point3'

kiwari module-attribute

kiwari = _KiwariFactory()

Declaration dataclass

Declaration(kind: str, default: Any, about: str = '', optional: bool = False, minimum: Optional[float] = None, maximum: Optional[float] = None, choices: Optional[Type[Enum]] = None)

One parameter: what it is, what it defaults to, and what it may be.

kind instance-attribute

kind: str

default instance-attribute

default: Any

about class-attribute instance-attribute

about: str = ''

optional class-attribute instance-attribute

optional: bool = False

minimum class-attribute instance-attribute

minimum: Optional[float] = None

maximum class-attribute instance-attribute

maximum: Optional[float] = None

choices class-attribute instance-attribute

choices: Optional[Type[Enum]] = None

__post_init__

__post_init__() -> None
Source code in kumiki/kiwari.py
def __post_init__(self) -> None:
    if self.kind not in _KINDS:
        raise ValueError(f"{self.kind!r} is not a kind of parameter. Expected one of {_KINDS}")
    if self.kind == CHOICE:
        if self.choices is None or not (isinstance(self.choices, type) and issubclass(self.choices, Enum)):
            raise ValueError("a choice must name the Enum class its options come from")
    if self.default is None and not self.optional:
        raise ValueError(
            "a parameter with no default has nothing to build with. "
            "Pass optional=True if it is genuinely allowed to be nothing."
        )

Kiwari dataclass

Kiwari(declarations: Mapping[str, Declaration], values: Mapping[str, Any] = dict(), texts: Mapping[str, str] = dict(), _origin: Optional['Kiwari'] = None)

What a frame is proportioned from: the declarations and their values.

Made by :func:kiwari. A bare one's values are its defaults; resolve returns a new one with incoming values laid over the top. Both halves are one type so an accessor always has the declaration to check itself against.

declarations instance-attribute

declarations: Mapping[str, Declaration]

values class-attribute instance-attribute

values: Mapping[str, Any] = field(default_factory=dict)

texts class-attribute instance-attribute

texts: Mapping[str, str] = field(default_factory=dict)

origin property

origin: 'Kiwari'

The kiwari as declared, before any values were laid over it.

Resolving makes a new object, so two frames built from one declared kiwari never hold the same one. They do share its origin, which is how kigumi tells they share their parameters.

__post_init__

__post_init__() -> None
Source code in kumiki/kiwari.py
def __post_init__(self) -> None:
    # Frozen stops the attributes being rebound, not the dicts behind them
    # being written to. A kiwari that quietly changed under a frame that
    # had already been built from it would be the worst kind of bug here.
    for name in ("declarations", "values", "texts"):
        object.__setattr__(self, name, MappingProxyType(dict(getattr(self, name))))

length

length(key: str) -> float

A distance, in metres.

Source code in kumiki/kiwari.py
def length(self, key: str) -> float:
    """A distance, in metres."""
    return self._look_up(key, LENGTH)

angle

angle(key: str) -> float

An angle, in radians.

Source code in kumiki/kiwari.py
def angle(self, key: str) -> float:
    """An angle, in radians."""
    return self._look_up(key, ANGLE)

count

count(key: str) -> int

A whole number of things.

Source code in kumiki/kiwari.py
def count(self, key: str) -> int:
    """A whole number of things."""
    return self._look_up(key, COUNT)

number

number(key: str) -> float

A plain dimensionless number.

Source code in kumiki/kiwari.py
def number(self, key: str) -> float:
    """A plain dimensionless number."""
    return self._look_up(key, NUMBER)

flag

flag(key: str) -> bool

On or off.

Source code in kumiki/kiwari.py
def flag(self, key: str) -> bool:
    """On or off."""
    return self._look_up(key, FLAG)

text

text(key: str) -> str

Free text.

Source code in kumiki/kiwari.py
def text(self, key: str) -> str:
    """Free text."""
    return self._look_up(key, TEXT)

choice

choice(key: str, choices: Optional[Type[Enum]] = None) -> Any

One of an Enum's members.

Pass the Enum class to be told at the call site if the declaration has changed under you.

Source code in kumiki/kiwari.py
def choice(self, key: str, choices: Optional[Type[Enum]] = None) -> Any:
    """One of an Enum's members.

    Pass the Enum class to be told at the call site if the declaration has
    changed under you.
    """
    value = self._look_up(key, CHOICE)
    declared = self.declarations[key].choices
    if choices is not None and declared is not choices:
        raise TypeError(
            f"{key!r} chooses from {declared.__name__ if declared else 'nothing'}, "
            f"not {choices.__name__}"
        )
    return value

v2

v2(key: str) -> V2

Two lengths.

Source code in kumiki/kiwari.py
def v2(self, key: str) -> V2:
    """Two lengths."""
    return self._look_up(key, POINT2)

v3

v3(key: str) -> V3

Three lengths.

Source code in kumiki/kiwari.py
def v3(self, key: str) -> V3:
    """Three lengths."""
    return self._look_up(key, POINT3)

resolve

resolve(incoming: Union['Kiwari', Mapping[str, Any], None]) -> 'Kiwari'

This kiwari with incoming's values laid over its own.

Takes another kiwari (whose values are used, not its declarations), a plain mapping of key to value, or None for "just the defaults". Every value is checked against this kiwari's declarations -- which is what makes editing the file while the viewer is open come out right: the declarations made on this run win, and values for keys that survived carry over.

A key that is no longer declared is dropped with a warning rather than raising: it means the file changed under a saved value, which the author cannot fix from inside the build.

Source code in kumiki/kiwari.py
def resolve(self, incoming: Union["Kiwari", Mapping[str, Any], None]) -> "Kiwari":
    """This kiwari with *incoming*'s values laid over its own.

    Takes another kiwari (whose values are used, not its declarations), a
    plain mapping of key to value, or None for "just the defaults". Every
    value is checked against *this* kiwari's declarations -- which is what
    makes editing the file while the viewer is open come out right: the
    declarations made on this run win, and values for keys that survived
    carry over.

    A key that is no longer declared is dropped with a warning rather than
    raising: it means the file changed under a saved value, which the
    author cannot fix from inside the build.
    """
    if incoming is None:
        return self

    # Recognised by shape, not by isinstance: the runner purges and
    # re-imports kumiki on every reload so source edits take effect, which
    # makes the Kiwari held from the previous build a different class
    # object from this one. It is still a kiwari.
    if hasattr(incoming, "declarations"):
        # `declarations` alone, because a plain dict has `.values` too --
        # as a method, which would read as an empty set of values rather
        # than failing loudly.
        arriving = dict(getattr(incoming, "values"))
        arriving_texts = dict(getattr(incoming, "texts", {}))
    elif isinstance(incoming, Mapping):
        arriving, arriving_texts = _split_values_and_texts(incoming)
    else:
        raise TypeError(
            f"a kiwari resolves against another kiwari or a mapping, not {type(incoming).__name__}"
        )

    stale = [key for key in arriving if key not in self.declarations]
    if stale:
        warnings.warn(
            f"{', '.join(sorted(stale))} {'is' if len(stale) == 1 else 'are'} no longer "
            f"declared by this kiwari, so {'it was' if len(stale) == 1 else 'they were'} "
            f"ignored. Declares: {', '.join(self.declarations) or 'nothing'}."
        )

    values = dict(self.values)
    texts = dict(self.texts)
    for key, declaration in self.declarations.items():
        if key not in arriving:
            continue
        values[key] = _coerce(key, declaration, arriving[key])
        written = arriving_texts.get(key)
        if written is not None:
            texts[key] = written
        else:
            texts.pop(key, None)
    return replace(self, values=values, texts=texts, _origin=self.origin)

default

default(key: str) -> Any

What key is when nobody has said otherwise.

Source code in kumiki/kiwari.py
def default(self, key: str) -> Any:
    """What *key* is when nobody has said otherwise."""
    return self.declarations[key].default

changed_from_defaults

changed_from_defaults() -> Tuple[str, ...]

The keys whose value is not what the code declares.

What the viewer highlights, and the only thing a companion file saves: writing every value out would mean changing a default in the source silently did nothing.

Source code in kumiki/kiwari.py
def changed_from_defaults(self) -> Tuple[str, ...]:
    """The keys whose value is not what the code declares.

    What the viewer highlights, and the only thing a companion file saves:
    writing every value out would mean changing a default in the source
    silently did nothing.
    """
    return tuple(
        key for key, declaration in self.declarations.items()
        if key in self.values and not _same_value(self.values[key], declaration.default)
    )

written

written(key: str) -> str

key's value as it would be typed.

What somebody actually typed, where they did. Writing a length back out is lossy in inches, so the text beats anything we could regenerate.

Source code in kumiki/kiwari.py
def written(self, key: str) -> str:
    """*key*'s value as it would be typed.

    What somebody actually typed, where they did. Writing a length back out
    is lossy in inches, so the text beats anything we could regenerate.
    """
    if key in self.texts:
        return self.texts[key]
    declaration = self.declarations[key]
    return _write(declaration, self.values.get(key, declaration.default))

to_payload

to_payload() -> Dict[str, Any]

The whole kiwari as JSON-able data, for the viewer.

Source code in kumiki/kiwari.py
def to_payload(self) -> Dict[str, Any]:
    """The whole kiwari as JSON-able data, for the viewer."""
    return {
        "schema": [
            _declaration_payload(key, declaration)
            for key, declaration in self.declarations.items()
        ],
        "applied": {
            key: self.value_payload(key)
            for key in self.declarations
        },
        "changed": list(self.changed_from_defaults()),
    }

value_payload

value_payload(key: str) -> Dict[str, Any]

One value, as the viewer holds it: the number, and how it was typed if anyone typed it.

Only typed text: anything written out here would be in mm, and the viewer writes an untyped value in the units it is set to.

Source code in kumiki/kiwari.py
def value_payload(self, key: str) -> Dict[str, Any]:
    """One value, as the viewer holds it: the number, and how it was typed if anyone typed it.

    Only typed text: anything written out here would be in mm, and the viewer writes
    an untyped value in the units it is set to.
    """
    declaration = self.declarations[key]
    value = self.values.get(key, declaration.default)
    payload: Dict[str, Any] = {"value": _plain(declaration, value)}
    written = self.texts.get(key)
    if written is not None:
        payload["text"] = written
    return payload