Skip to content

Properties

Property

A single <property>: a typed, named value on a control.

A property's Python value is always a native type -- bool, int, float, str, Frame or Color -- and is converted to text only when the file is written.

Attributes:

Name Type Description
key

The camelCase key as stored in the file.

type

Which <property type=> this is written as.

value Any

The native Python value.

Source code in src/py2tosc/properties.py
class Property:
    """A single `<property>`: a typed, named value on a control.

    A property's Python value is always a native type -- `bool`, `int`, `float`,
    `str`, [`Frame`][py2tosc.Frame] or [`Color`][py2tosc.Color] -- and is
    converted to text only when the file is written.

    Attributes:
        key: The camelCase key as stored in the file.
        type: Which `<property type=>` this is written as.
        value: The native Python value.
    """

    __slots__ = ("_value", "key", "type")

    def __init__(self, key: str, value: Any, type: PropertyType | str | None = None):
        """
        Args:
            key: The property key, in either `snake_case` or camelCase.
            value: The value to store.
            type: Force a property type instead of inferring one. Rarely needed.
        """
        self.key = to_camel(key)
        self.type = (
            PropertyType(type) if type is not None else infer_type(self.key, value)
        )
        self._value = self._coerce(value)

    def _coerce(self, value: Any) -> Any:
        match self.type:
            case PropertyType.FRAME:
                return to_frame(value)
            case PropertyType.COLOR:
                return to_color(value)
            case PropertyType.BOOLEAN:
                return bool(value)
            case PropertyType.INTEGER:
                return int(value)
            case PropertyType.FLOAT:
                return float(value)
            case _:
                return str(value)

    @property
    def value(self) -> Any:
        """The native Python value, coerced to match `type`."""
        return self._value

    @value.setter
    def value(self, new: Any) -> None:
        self._value = self._coerce(new)

    @property
    def python_name(self) -> str:
        """The `snake_case` name this property is reachable by."""
        return to_snake(self.key)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Property):
            return NotImplemented
        return (self.key, self.type, self._value) == (
            other.key,
            other.type,
            other._value,
        )

    def __hash__(self) -> int:
        return hash((self.key, self.type, self._value))

    def __repr__(self) -> str:
        return f"Property({self.key!r}, {self._value!r}, {self.type.value!r})"

value property writable

value: Any

The native Python value, coerced to match type.

python_name property

python_name: str

The snake_case name this property is reachable by.

Frame

Bases: NamedTuple

A control's position and size, in points.

Components are floats: TouchOSC positions controls at sub-pixel offsets, and its own layouts are full of frames like x=417.439. Rounding them would move every such control.

Comparable and unpackable as a plain (x, y, w, h) tuple, and an integral frame compares equal to a tuple of ints.

Source code in src/py2tosc/properties.py
class Frame(NamedTuple):
    """A control's position and size, in points.

    Components are floats: TouchOSC positions controls at sub-pixel offsets, and
    its own layouts are full of frames like `x=417.439`. Rounding them would
    move every such control.

    Comparable and unpackable as a plain `(x, y, w, h)` tuple, and an integral
    frame compares equal to a tuple of ints.
    """

    x: float
    y: float
    w: float
    h: float

Color

Bases: NamedTuple

An RGBA colour with components from 0.0 to 1.0.

Comparable and unpackable as a plain (r, g, b, a) tuple.

Source code in src/py2tosc/properties.py
class Color(NamedTuple):
    """An RGBA colour with components from 0.0 to 1.0.

    Comparable and unpackable as a plain `(r, g, b, a)` tuple.
    """

    r: float
    g: float
    b: float
    a: float

to_frame

to_frame(value: Any) -> Frame

Coerce a 4-item sequence into a Frame.

Parameters:

Name Type Description Default
value Any

Any sequence of four numbers.

required

Returns:

Type Description
Frame

The frame, with components as floats. They are not rounded: TouchOSC

Frame

stores sub-pixel positions and rounding would move the control.

Raises:

Type Description
ValueError

If value does not hold exactly four items.

Source code in src/py2tosc/properties.py
def to_frame(value: Any) -> Frame:
    """Coerce a 4-item sequence into a [`Frame`][py2tosc.Frame].

    Args:
        value: Any sequence of four numbers.

    Returns:
        The frame, with components as floats. They are not rounded: TouchOSC
        stores sub-pixel positions and rounding would move the control.

    Raises:
        ValueError: If `value` does not hold exactly four items.
    """
    if isinstance(value, Frame):
        return value
    items = tuple(value)
    if len(items) != 4:
        raise ValueError(f"a frame needs 4 values (x, y, w, h), got {len(items)}")
    return Frame(*(float(i) for i in items))

to_color

to_color(value: Any) -> Color

Coerce a colour in any accepted notation into a Color.

Accepts floats already in 0.0-1.0, integers in 0-255, and hex strings with or without a leading # and with or without an alpha pair.

Parameters:

Name Type Description Default
value Any

(1.0, 0.0, 0.0, 1.0), (255, 0, 0, 255), "#ff0000" or "#ff0000ff".

required

Returns:

Type Description
Color

The colour normalised to 0.0-1.0.

Raises:

Type Description
ValueError

If the string is not 6 or 8 hex digits, or the sequence does not hold three or four items.

Source code in src/py2tosc/properties.py
def to_color(value: Any) -> Color:
    """Coerce a colour in any accepted notation into a [`Color`][py2tosc.Color].

    Accepts floats already in 0.0-1.0, integers in 0-255, and hex strings with
    or without a leading `#` and with or without an alpha pair.

    Args:
        value: `(1.0, 0.0, 0.0, 1.0)`, `(255, 0, 0, 255)`, `"#ff0000"` or
            `"#ff0000ff"`.

    Returns:
        The colour normalised to 0.0-1.0.

    Raises:
        ValueError: If the string is not 6 or 8 hex digits, or the sequence does
            not hold three or four items.
    """
    if isinstance(value, Color):
        return value

    if isinstance(value, str):
        text = value.lstrip("#")
        if len(text) not in (6, 8):
            raise ValueError(f"{value!r} is not a 6 or 8 digit hex colour")
        pairs = [text[i : i + 2] for i in range(0, len(text), 2)]
        components = [int(p, 16) / 255 for p in pairs]
        if len(components) == 3:
            components.append(1.0)
        return Color(*components)

    items = tuple(value)
    if len(items) not in (3, 4):
        raise ValueError(f"a colour needs 3 or 4 values, got {len(items)}")

    rgb = items[:3]
    alpha = items[3] if len(items) == 4 else None

    # The scale is decided by the RGB components alone. Integers above 1 mean
    # 0-255; anything else is already normalised. A tuple of ints that are all
    # 0 or 1 is ambiguous, and (0, 0, 0, 1) is a far more common way to write
    # opaque black than "almost transparent almost-black".
    scaled = all(isinstance(i, int) for i in rgb) and any(i > 1 for i in rgb)
    components = [i / 255 if scaled else float(i) for i in rgb]

    # Alpha is judged on its own: in 0-255 notation an alpha of 1 still reads as
    # "opaque", because nobody writes 0.4% opacity as an integer.
    if alpha is None:
        components.append(1.0)
    elif isinstance(alpha, int) and alpha > 1:
        components.append(alpha / 255)
    else:
        components.append(float(alpha))

    return Color(*components)

to_camel

to_camel(name: str) -> str

Convert a snake_case Python name to its camelCase format key.

Names that contain no underscore are returned unchanged, so already-camelCase keys pass through untouched.

Parameters:

Name Type Description Default
name str

A property name in either convention.

required

Returns:

Type Description
str

The camelCase key as it is stored in the file.

Source code in src/py2tosc/properties.py
def to_camel(name: str) -> str:
    """Convert a `snake_case` Python name to its camelCase format key.

    Names that contain no underscore are returned unchanged, so already-camelCase
    keys pass through untouched.

    Args:
        name: A property name in either convention.

    Returns:
        The camelCase key as it is stored in the file.
    """
    return _SNAKE_BOUNDARY.sub(lambda m: m.group(1).upper(), name)

to_snake

to_snake(key: str) -> str

Convert a camelCase format key to its snake_case Python name.

Parameters:

Name Type Description Default
key str

A property key as stored in the file.

required

Returns:

Type Description
str

The snake_case equivalent.

Source code in src/py2tosc/properties.py
def to_snake(key: str) -> str:
    """Convert a camelCase format key to its `snake_case` Python name.

    Args:
        key: A property key as stored in the file.

    Returns:
        The `snake_case` equivalent.
    """
    return _CAMEL_BOUNDARY.sub("_", key).lower()