FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

GitHub Viewer

"""The typed [`Style`][pythonnative.Style], style resolution, and style sheets. PythonNative ships a single, fully typed [`Style`][pythonnative.Style] ``TypedDict`` that enumerates every supported style property and constrains enum-shaped values with [`typing.Literal`][typing.Literal]. Editors and type checkers (mypy, pyright) autocomplete and validate every key: ``flex_direction="collumn"`` is a static error, not a silent runtime no-op. Style values are plain dicts at runtime, so they're trivial to compose, diff, and store. Pass one dict or a list of them (later entries win, ``None`` entries are skipped); [`resolve_style`][pythonnative.style.resolve_style] flattens the list and resolves the ``inset*`` shorthands. Group related styles in a [`StyleSheet`][pythonnative.StyleSheet] namespace, and derive theme-dependent styles with [`use_styles`][pythonnative.use_styles]: ```python import pythonnative as pn class Styles(pn.StyleSheet): container = pn.style(padding=16, gap=12) title = pn.style(font_size=24, font_weight="700") pn.Column(pn.Text("Hello", style=Styles.title), style=Styles.container) ``` """ import difflib from typing import ( Any, Dict, List, Literal, NotRequired, Optional, Sequence, Tuple, TypedDict, Union, Unpack, get_args, ) from . import diagnostics # ====================================================================== # Atomic value types # ====================================================================== class DynamicColor(TypedDict): """A color pair resolved against the current color scheme. Every renderer (iOS, Android, and the browser preview) picks ``light`` or ``dark`` when the style is applied and switches when the appearance changes. """ light: str dark: str Color = Union[str, DynamicColor] """Color value: ``"#RRGGBB"``, ``"#AARRGGBB"``, any string a platform handler recognizes (e.g., ``"red"``), or a [`DynamicColor`][pythonnative.DynamicColor] ``{"light": ..., "dark": ...}`` pair. Strings are stored verbatim and parsed by the handler at apply-time, so palettes from third-party libraries pass through unchanged.""" Dimension = Union[int, float, str] """A length value. Numbers are points/dp; strings ending in ``"%"`` are parent-relative percentages.""" class EdgeInsets(TypedDict, total=False): """Per-edge spacing values used for ``padding`` and ``margin``. Mirrors React Native's ``EdgeInsets`` shape with PythonNative's convenience aliases. Any subset of keys may be supplied. """ top: Dimension right: Dimension bottom: Dimension left: Dimension horizontal: Dimension vertical: Dimension all: Dimension EdgeValue = Union[Dimension, EdgeInsets] """Padding/margin value: a uniform number, a ``"%"`` string, or an [`EdgeInsets`][pythonnative.EdgeInsets] dict.""" class AccessibilityState(TypedDict, total=False): """Current widget state exposed to assistive technology. Passed via the ``accessibility_state=`` prop on interactive components. All keys are optional; omitted keys are treated as unset (not ``False``). Attributes: disabled: The widget is visible but not interactive. selected: The widget is currently selected (e.g. the active tab). checked: Toggle/checkbox state. ``"mixed"`` represents a tri-state checkbox. busy: The widget is loading or otherwise temporarily busy. expanded: A disclosure/accordion is currently expanded. """ disabled: bool selected: bool checked: Union[bool, Literal["mixed"]] busy: bool expanded: bool class AccessibilityValue(TypedDict, total=False): """The current value of a range-like or text-valued widget, for assistive technology. Passed via ``accessibility_value=`` (a plain string is also accepted and equivalent to ``{"text": ...}``). Use ``min``, ``max``, and ``now`` for sliders and progress indicators, ``text`` for everything else. Attributes: min: Lower bound of the range. max: Upper bound of the range. now: Current value within the range. text: Spoken description of the current value. """ min: float max: float now: float text: str class AccessibilityAction(TypedDict): """One custom action a screen reader may invoke on a view. Passed via ``accessibility_actions=[...]``; the view's ``on_accessibility_action`` callback receives the action ``name``. The standard names ``"activate"``, ``"increment"``, ``"decrement"``, ``"longpress"``, ``"magicTap"``, and ``"escape"`` map to the platform's built-in actions; any other name is a custom action announced with ``label``. Attributes: name: Identifier reported to ``on_accessibility_action``. label: Spoken description of a custom action. """ name: str label: NotRequired[str] ImportantForAccessibility = Literal["auto", "yes", "no", "no_hide_descendants"] """``important_for_accessibility`` value: whether assistive technology sees the view (``"yes"``), skips it (``"no"``), skips it and its subtree (``"no_hide_descendants"``), or decides itself (``"auto"``).""" class ShadowOffset(TypedDict): """Shadow displacement in points.""" width: float height: float ShadowOffsetValue = Union[ShadowOffset, Tuple[float, float], List[float]] """``shadow_offset`` / ``text_shadow_offset`` value: an [`ShadowOffset`][pythonnative.ShadowOffset] dict or an ``(x, y)`` pair.""" # ---------------------------------------------------------------------- # Transforms # ---------------------------------------------------------------------- class TransformRotate(TypedDict): """Rotation around the z axis in degrees (numeric) or with an explicit unit suffix.""" rotate: Union[float, str] class TransformRotateX(TypedDict): """Rotation around the x axis (a 3D tilt) in degrees or with a unit suffix.""" rotate_x: Union[float, str] class TransformRotateY(TypedDict): """Rotation around the y axis (a 3D flip) in degrees or with a unit suffix.""" rotate_y: Union[float, str] class TransformRotateZ(TypedDict): """Rotation around the z axis; an explicit spelling of ``rotate``.""" rotate_z: Union[float, str] class TransformScale(TypedDict): """Uniform scale transform.""" scale: float class TransformScaleX(TypedDict): """Horizontal-only scale transform.""" scale_x: float class TransformScaleY(TypedDict): """Vertical-only scale transform.""" scale_y: float class TransformTranslate(TypedDict, total=False): """Translation transform along one or both axes.""" translate_x: float translate_y: float class TransformSkewX(TypedDict): """Horizontal skew in degrees or with a unit suffix.""" skew_x: Union[float, str] class TransformSkewY(TypedDict): """Vertical skew in degrees or with a unit suffix.""" skew_y: Union[float, str] class TransformPerspective(TypedDict): """Perspective distance for the 3D rotations that follow it in the list.""" perspective: float TransformEntry = Union[ TransformRotate, TransformRotateX, TransformRotateY, TransformRotateZ, TransformScale, TransformScaleX, TransformScaleY, TransformTranslate, TransformSkewX, TransformSkewY, TransformPerspective, Dict[str, Any], ] """A single transform operation. Every renderer supports the whole set; iOS composes them into a ``CATransform3D``.""" TransformSpec = Union[TransformEntry, List[TransformEntry]] """``transform`` style value: a single operation or an ordered list.""" # ---------------------------------------------------------------------- # Enum-shaped style values # ---------------------------------------------------------------------- FlexDirection = Literal["row", "column", "row_reverse", "column_reverse"] FlexWrap = Literal["nowrap", "wrap", "wrap_reverse"] AlignContent = Literal[ "flex_start", "center", "flex_end", "stretch", "space_between", "space_around", "space_evenly", ] LayoutDirection = Literal["ltr", "rtl"] JustifyContent = Literal[ "flex_start", "center", "flex_end", "space_between", "space_around", "space_evenly", "start", "leading", "top", "end", "trailing", "bottom", ] AlignItems = Literal[ "stretch", "flex_start", "center", "flex_end", "baseline", "auto", "start", "leading", "top", "end", "trailing", "bottom", "fill", ] AlignSelf = AlignItems Position = Literal["relative", "absolute"] Display = Literal["flex", "none"] """``display`` style value. ``"none"`` removes the node and its subtree from layout entirely (no size, gap, or margin contribution; every frame in the subtree is ``(0, 0, 0, 0)``).""" Overflow = Literal["visible", "hidden", "scroll"] TextAlign = Literal["left", "center", "right", "justify", "start", "end"] TextDecoration = Literal["none", "underline", "line_through"] TextTransform = Literal["none", "uppercase", "lowercase", "capitalize"] """``text_transform`` style value. Applied in Python before the string reaches the native label, so measurement and rendering agree.""" MarginValue = Union[Dimension, Literal["auto"]] """A margin length: points, a ``"%"`` string, or ``"auto"`` to absorb free space.""" FontWeight = Literal[ "normal", "bold", "100", "200", "300", "400", "500", "600", "700", "800", "900", ] ScaleType = Literal["cover", "contain", "stretch", "center"] BorderStyle = Literal["solid", "dashed", "dotted"] """``border_style`` style value, drawn the same way on every renderer.""" KeyboardType = Literal[ "default", "email_address", "number_pad", "decimal_pad", "phone_pad", "url", "ascii", "numbers_and_punctuation", "web_search", "visible_password", ] """``keyboard_type`` value. ``"ascii"``, ``"numbers_and_punctuation"``, and ``"web_search"`` are iOS keyboards (Android and the browser fall back to the closest input type); ``"visible_password"`` is Android's unmasked password keyboard.""" AutoCapitalize = Literal["none", "sentences", "words", "characters"] ReturnKeyType = Literal["default", "done", "go", "next", "send", "search"] PointerEvents = Literal["auto", "none", "box_none", "box_only"] """Touch-handling mode for a view and its subtree: - ``"auto"``: the view and its children receive touches (default). - ``"none"``: neither the view nor its children receive touches; touches pass through to whatever is underneath. - ``"box_none"``: the view itself ignores touches but its children still receive them (decorative overlays with interactive content). - ``"box_only"``: the view receives touches but its children do not. """ # ====================================================================== # Style TypedDict # ====================================================================== class Style(TypedDict, total=False): """Statically-typed style dictionary. Lists every style property recognized by the built-in components plus their layout engine, with enum-shaped values constrained via [`Literal`][typing.Literal]. ``Style`` is a `total=False` TypedDict so any subset of keys is valid at construction time. Custom native components may accept additional, unlisted keys; they are ignored by the built-in handlers but flow through the reconciler unmodified, so third-party handlers can read them. Example: ```python import pythonnative as pn title: pn.Style = { "font_size": 24, "color": "#0A84FF", "text_align": "center", } pn.Text("Hello", style=title) ``` """ # --- Layout: sizing --- width: Dimension height: Dimension min_width: Dimension max_width: Dimension min_height: Dimension max_height: Dimension aspect_ratio: float # --- Layout: flex --- flex: float flex_grow: float flex_shrink: float flex_basis: Dimension flex_direction: FlexDirection flex_wrap: FlexWrap justify_content: JustifyContent align_items: AlignItems align_self: AlignSelf align_content: AlignContent direction: LayoutDirection display: Display # --- Layout: position --- position: Position top: Dimension right: Dimension bottom: Dimension left: Dimension start: Dimension end: Dimension # Shorthands resolved by ``resolve_style`` into the four edges above; # explicit ``top`` / ``right`` / ``bottom`` / ``left`` keys win. inset: Dimension inset_horizontal: Dimension inset_vertical: Dimension # --- Layout: spacing --- padding: EdgeValue padding_top: Dimension padding_bottom: Dimension padding_left: Dimension padding_right: Dimension padding_start: Dimension padding_end: Dimension padding_horizontal: Dimension padding_vertical: Dimension margin: Union[MarginValue, EdgeInsets] margin_top: MarginValue margin_bottom: MarginValue margin_left: MarginValue margin_right: MarginValue margin_start: MarginValue margin_end: MarginValue margin_horizontal: MarginValue margin_vertical: MarginValue gap: float row_gap: float column_gap: float # --- Visual: clipping & overflow --- overflow: Overflow # --- Visual: colors --- background_color: Color color: Color border_color: Color placeholder_color: Color tint_color: Color # --- Visual: borders --- border_width: float border_style: BorderStyle border_radius: float border_top_left_radius: float border_top_right_radius: float border_bottom_left_radius: float border_bottom_right_radius: float border_top_width: float border_right_width: float border_bottom_width: float border_left_width: float border_top_color: Color border_right_color: Color border_bottom_color: Color border_left_color: Color # --- Visual: typography --- font_size: float font_family: str font_weight: FontWeight bold: bool italic: bool text_align: TextAlign text_decoration: TextDecoration text_transform: TextTransform line_height: float letter_spacing: float max_lines: int text_shadow_color: Color text_shadow_offset: ShadowOffsetValue text_shadow_radius: float # --- Visual: shadows / effects --- # Shadows follow React Native's platform split: the ``shadow_*`` # keys draw on iOS and in the browser preview, ``elevation`` draws # Android's material shadow. Each platform ignores the other's keys. shadow_color: Color shadow_offset: ShadowOffsetValue shadow_opacity: float shadow_radius: float elevation: float opacity: float transform: TransformSpec # --- Interaction & stacking --- z_index: int pointer_events: PointerEvents StyleProp = Union[Style, Sequence[Optional[Style]], None] """Public type for the ``style`` parameter on every component factory. Accepts a [`Style`][pythonnative.Style] TypedDict, a sequence of them with ``None`` entries skipped, or ``None``. ``Style`` is a ``TypedDict``, so a plain dict literal type-checks against it and still works at runtime.""" # ====================================================================== # Runtime helpers # ====================================================================== def style(**properties: Unpack[Style]) -> Style: """Construct a [`Style`][pythonnative.Style] from keyword arguments. Equivalent to ``Style(...)`` but reads more naturally inside expressions, and typed with ``Unpack[Style]`` so a type checker rejects a misspelled key such as ``pn.style(colour="red")``: ```python pn.View(child, style=pn.style(padding=16, background_color="#fff")) ``` At runtime the keys aren't checked here; the factories warn once per unknown key in dev mode through [`validate_style_keys`][pythonnative.style.validate_style_keys]. Args: **properties: Style key/value pairs. Returns: A fresh ``Style`` dict containing the supplied entries. """ return properties _INSET_SHORTHANDS: Dict[str, Tuple[str, ...]] = { "inset": ("top", "right", "bottom", "left"), "inset_horizontal": ("left", "right"), "inset_vertical": ("top", "bottom"), } """Position shorthands and the edges each one fills, least specific first.""" def _expand_insets(result: Dict[str, Any]) -> None: """Replace ``inset*`` shorthands with the four edge keys, in place. Mirrors the padding vocabulary: ``inset`` fills every edge, the axis shorthands override it, and an explicit ``top`` / ``right`` / ``bottom`` / ``left`` wins over both, regardless of key order. The wire only ever carries the four edges. """ explicit: Dict[str, Any] = {edge: result[edge] for edge in ("top", "right", "bottom", "left") if edge in result} for shorthand, edges in _INSET_SHORTHANDS.items(): if shorthand not in result: continue value = result.pop(shorthand) if value is None: continue for edge in edges: result[edge] = value result.update(explicit) def resolve_style(value: StyleProp) -> Dict[str, Any]: """Flatten a `style` prop into a single dict. Accepts ``None``, a single dict, or a sequence of dicts (later entries override earlier ones, mirroring React Native's array-style pattern), then resolves the ``inset``, ``inset_horizontal``, and ``inset_vertical`` shorthands into ``top`` / ``right`` / ``bottom`` / ``left``. Used by every built-in element factory in `pythonnative.components`. Args: value: The raw value of the component's `style` argument. Returns: A flat dict suitable for the native handler. Always a fresh dict, never the input. """ if value is None: return {} if isinstance(value, dict): result: Dict[str, Any] = dict(value) else: result = {} for entry in value: if entry: result.update(entry) if result.keys() & _INSET_SHORTHANDS.keys(): _expand_insets(result) return result _KNOWN_STYLE_KEYS = frozenset(Style.__annotations__) """Every key declared on the [`Style`][pythonnative.Style] TypedDict.""" _STYLE_VALUE_CHOICES: Dict[str, frozenset] = { "display": frozenset(get_args(Display)), "position": frozenset(get_args(Position)), "direction": frozenset(get_args(LayoutDirection)), "text_decoration": frozenset(get_args(TextDecoration)), "text_transform": frozenset(get_args(TextTransform)), "pointer_events": frozenset(get_args(PointerEvents)), "border_style": frozenset(get_args(BorderStyle)), } """Keys whose string values are checked against their ``Literal`` choices in dev mode. Only keys whose handlers accept exactly the declared literals are listed, so friendly aliases (``align_items: "leading"``, ``font_weight: "semibold"``) never trigger false positives.""" def _is_number(value: Any) -> bool: return isinstance(value, (int, float)) and not isinstance(value, bool) def _text_shadow_offset_ok(value: Any) -> bool: if isinstance(value, dict): return all(_is_number(value.get(k, 0)) for k in ("width", "height")) if isinstance(value, (tuple, list)) and len(value) == 2: return all(_is_number(v) for v in value) return False def _bad_value_message(key: str, value: Any) -> Optional[str]: """Return a description of why ``value`` is invalid for ``key``, or ``None``.""" choices = _STYLE_VALUE_CHOICES.get(key) if choices is not None: if not isinstance(value, str) or value not in choices: allowed = ", ".join(repr(c) for c in sorted(choices)) return f"Invalid value {value!r} for style key {key!r}; expected one of {allowed}." return None if key == "text_shadow_offset" and not _text_shadow_offset_ok(value): return ( f"Invalid value {value!r} for style key 'text_shadow_offset'; " "expected {'width': x, 'height': y} or an (x, y) pair." ) if key == "text_shadow_radius" and (not _is_number(value) or value < 0): return f"Invalid value {value!r} for style key 'text_shadow_radius'; expected a non-negative number." return None def validate_style_keys(style_dict: Dict[str, Any], owner: str = "") -> None: """Warn (once per key) about style keys no built-in handler reads. Dev-mode only; production skips the scan entirely. Typos get a "did you mean" suggestion via fuzzy matching against the declared [`Style`][pythonnative.Style] keys. Unknown keys still flow through to handlers untouched, so custom components that read extra keys keep working (at the cost of one dev warning). A small set of enum-shaped keys (``display``, ``position``, ``direction``, ``text_decoration``, ``text_transform``, ``pointer_events``, ``border_style``) and the ``text_shadow_*`` keys also have their values checked; a bad value warns once per key/value pair and is otherwise passed through (the engine and handlers ignore values they don't recognize). Args: style_dict: The flattened style dict about to be merged into an element's props. owner: Element type name for the warning message (e.g. ``"Text"``). """ if not diagnostics.is_dev() or not style_dict: return for key, value in style_dict.items(): if key in _KNOWN_STYLE_KEYS: if value is None: continue problem = _bad_value_message(key, value) if problem is not None: diagnostics.warn_once( problem + (f" (on {owner})" if owner else ""), key=f"style-value:{owner}:{key}:{value!r}", ) continue matches = difflib.get_close_matches(str(key), _KNOWN_STYLE_KEYS, n=1) hint = f" Did you mean {matches[0]!r}?" if matches else "" diagnostics.warn_once( f"Unknown style key {key!r}" + (f" on {owner}" if owner else "") + f".{hint} Built-in components ignore keys they don't recognize.", key=f"style:{owner}:{key}", ) # ====================================================================== # StyleSheet # ====================================================================== class StyleSheet: """Base class for a namespace of named styles. Subclass it and assign [`Style`][pythonnative.Style] dicts as class attributes. Attribute access is typed, so a misspelled style name is a static error, and every style is checked for unknown keys once in dev mode: ```python class Styles(pn.StyleSheet): card = pn.style(padding=16, border_radius=12) title = pn.style(font_size=17, font_weight="600") pn.View(pn.Text("Hello", style=Styles.title), style=Styles.card) ``` Compose styles with a list (``style=[Styles.card, {"opacity": 0.5}]``). For styles derived from the theme, see [`use_styles`][pythonnative.use_styles]. A style sheet is a namespace, not an object: instantiating one raises ``TypeError``. """ def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) for name, value in vars(cls).items(): if not name.startswith("_") and isinstance(value, dict): validate_style_keys(value, owner=f"{cls.__name__}.{name}") def __new__(cls, *args: Any, **kwargs: Any) -> "StyleSheet": """Refuse instantiation: a style sheet is a namespace, not an object.""" raise TypeError(f"{cls.__name__} is a namespace of styles; use {cls.__name__}. instead of calling it") ABSOLUTE_FILL: Style = {"position": "absolute", "top": 0, "right": 0, "bottom": 0, "left": 0} """A style that absolutely fills the parent, like React Native's ``StyleSheet.absoluteFill``.""" __all__ = [ "ABSOLUTE_FILL", "AccessibilityAction", "AccessibilityState", "AccessibilityValue", "AlignItems", "AlignSelf", "AutoCapitalize", "BorderStyle", "Color", "Dimension", "Display", "DynamicColor", "EdgeInsets", "EdgeValue", "FlexDirection", "FontWeight", "ImportantForAccessibility", "JustifyContent", "KeyboardType", "MarginValue", "Overflow", "PointerEvents", "Position", "ReturnKeyType", "ScaleType", "ShadowOffset", "ShadowOffsetValue", "Style", "StyleProp", "StyleSheet", "TextAlign", "TextDecoration", "TextTransform", "TransformEntry", "TransformPerspective", "TransformRotate", "TransformRotateX", "TransformRotateY", "TransformRotateZ", "TransformScale", "TransformScaleX", "TransformScaleY", "TransformSkewX", "TransformSkewY", "TransformSpec", "TransformTranslate", "resolve_style", "style", "validate_style_keys", ]

Back | FazBrowse Home | New Git URL