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

GitHub Viewer

"""StyleSheet, typed `Style`, style resolution, and theming. PythonNative ships a single, fully-typed [`Style`][pythonnative.Style] TypedDict that enumerates every supported style property and constrains enum-shaped values via [`typing.Literal`][typing.Literal]. The TypedDict gives editors and type-checkers (mypy, pyright) full autocomplete and validation: a typo such as ``flex_direction="collumn"`` is now a static error, not a silent runtime no-op. Style values remain plain dicts at runtime so they are trivial to compose, diff, and store. Properties unrecognized by a platform handler are still ignored, so third-party handlers may extend the palette without modifying core types. The runtime helpers ([`resolve_style`][pythonnative.style.resolve_style], [`StyleSheet`][pythonnative.StyleSheet]) accept either a ``Style`` TypedDict, a regular ``Dict[str, Any]`` (for forward-compat or unrestricted use), or a list of either kind of dict, and always return a fresh, flat dict. Example: ```python import pythonnative as pn styles = pn.StyleSheet.create( title=pn.style(font_size=24, bold=True, color="#333"), container=pn.style(padding=16, spacing=12), ) pn.Column( pn.Text("Hello", style=styles["title"]), style=styles["container"], ) ``` """ import dataclasses import difflib from dataclasses import dataclass from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union, get_args from . import diagnostics from .hooks import Context, create_context, use_color_scheme, use_context # ====================================================================== # Atomic value types # ====================================================================== Color = str """Color value: ``"#RRGGBB"``, ``"#AARRGGBB"``, or any string a platform handler recognizes (e.g., ``"red"``). 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 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 transform in degrees (numeric) or with explicit unit suffix.""" rotate: 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 TransformEntry = Union[ TransformRotate, TransformScale, TransformScaleX, TransformScaleY, TransformTranslate, Dict[str, Any], ] """A single transform operation.""" 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"] KeyboardType = Literal[ "default", "email_address", "number_pad", "decimal_pad", "phone_pad", "url", ] 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 # --- 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 spacing: float 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_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 --- 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, Dict[str, Any], List[Optional[Union[Style, Dict[str, Any]]]], None] """Public type for the ``style`` parameter on every component factory. Accepts a [`Style`][pythonnative.Style] TypedDict (recommended), a plain ``Dict[str, Any]`` (forward-compat / unrestricted), a list of either with ``None`` entries skipped, or ``None``.""" # ====================================================================== # Runtime helpers # ====================================================================== def style(**properties: Any) -> Style: """Construct a [`Style`][pythonnative.Style] from keyword arguments. Equivalent to ``Style(...)`` but works with any Python version (``TypedDict.__init__`` is fragile prior to 3.11) and reads more naturally inside expressions: ```python pn.View(child, style=pn.style(padding=16, background_color="#fff")) ``` Unknown keys are accepted at runtime to keep the door open for third-party styling extensions; static type checkers will still reject them when this helper's return type is annotated as ``Style``. Args: **properties: Style key/value pairs. Returns: A fresh ``Style`` dict containing the supplied entries. """ return properties # type: ignore[return-value] def resolve_style(value: StyleProp) -> Dict[str, Any]: """Flatten a `style` prop into a single dict. Accepts ``None``, a single dict (``Style`` or untyped), or a list of dicts (later entries override earlier ones, mirroring React Native's array-style pattern). 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): return dict(value) result: Dict[str, Any] = {} for entry in value: if entry: result.update(entry) 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)), } """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``) 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: """Utility for creating, composing, and flattening style dictionaries. All methods are stateless and return fresh dicts, so the values can be reused safely across components. """ @staticmethod def create(**named_styles: Style) -> Dict[str, Style]: """Create a set of named styles from keyword arguments. Args: **named_styles: Each keyword argument is a style name mapping to a [`Style`][pythonnative.Style] dict. Returns: A dict mapping each name to a copy of the supplied style, so the caller can mutate the result without affecting the originals. Example: ```python from pythonnative import StyleSheet, style styles = StyleSheet.create( heading=style(font_size=28, bold=True), body=style(font_size=16), ) ``` """ return {name: dict(props) for name, props in named_styles.items()} # type: ignore[misc] @staticmethod def compose(*styles: StyleProp) -> Style: """Merge multiple style dicts. Args: *styles: Style dicts to merge. Later dicts override keys from earlier ones. Falsy entries (``None``) are skipped. List entries are flattened in turn. Returns: A new ``Style`` dict containing the merged result. """ merged: Dict[str, Any] = {} for entry in styles: if entry is None: continue if isinstance(entry, dict): merged.update(entry) continue for nested in entry: if nested: merged.update(nested) return merged # type: ignore[return-value] @staticmethod def flatten(styles: StyleProp) -> Style: """Flatten a style value or list of styles into a single dict. Equivalent to [`resolve_style`][pythonnative.style.resolve_style] but exposed on `StyleSheet` for parity with React Native's API and typed as returning a ``Style``. Args: styles: A single dict, a list of dicts, or `None`. Returns: A flat ``Style`` dict combining the inputs. """ return resolve_style(styles) # type: ignore[return-value] @staticmethod def absolute_fill() -> Style: """Return a style that absolutely fills the parent. Convenience preset matching React Native's ``StyleSheet.absoluteFill``: ``position: "absolute"`` with every inset pinned to ``0``. """ return {"position": "absolute", "top": 0, "right": 0, "bottom": 0, "left": 0} # ====================================================================== # Theming # ====================================================================== @dataclass(frozen=True) class Theme: """Design tokens read through [`use_theme`][pythonnative.use_theme]. A ``Theme`` is an immutable, fully typed record, so ``theme.text_color`` autocompletes and a typo is a static error rather than a runtime ``KeyError``. Derive a custom theme from a built-in one with [`replace`][pythonnative.style.Theme.replace]: ```python brand = pn.DEFAULT_LIGHT_THEME.replace(primary_color="#FF2D55") ``` Attributes: primary_color: Main accent color. secondary_color: Secondary accent color. background_color: Screen background. surface_color: Raised surfaces such as cards and sheets. text_color: Primary text. text_secondary_color: De-emphasized text. error_color: Destructive and error states. success_color: Success states. warning_color: Warning states. font_size: Body text size, in points. font_size_small: Caption size, in points. font_size_large: Subtitle size, in points. font_size_title: Title size, in points. spacing: Base spacing unit, in points. spacing_large: Large spacing unit, in points. border_radius: Default corner radius, in points. """ primary_color: Color secondary_color: Color background_color: Color surface_color: Color text_color: Color text_secondary_color: Color error_color: Color success_color: Color warning_color: Color font_size: float = 16 font_size_small: float = 13 font_size_large: float = 20 font_size_title: float = 28 spacing: float = 8 spacing_large: float = 16 border_radius: float = 8 def replace(self, **changes: Any) -> "Theme": """Return a copy of this theme with the given fields replaced. Raises: TypeError: If ``changes`` names a field that doesn't exist. """ return dataclasses.replace(self, **changes) DEFAULT_LIGHT_THEME = Theme( primary_color="#007AFF", secondary_color="#5856D6", background_color="#FFFFFF", surface_color="#F2F2F7", text_color="#000000", text_secondary_color="#8E8E93", error_color="#FF3B30", success_color="#34C759", warning_color="#FF9500", ) """Built-in light theme selected by [`use_theme`][pythonnative.use_theme].""" DEFAULT_DARK_THEME = Theme( primary_color="#0A84FF", secondary_color="#5E5CE6", background_color="#000000", surface_color="#1C1C1E", text_color="#FFFFFF", text_secondary_color="#8E8E93", error_color="#FF453A", success_color="#30D158", warning_color="#FF9F0A", ) """Built-in dark theme selected by [`use_theme`][pythonnative.use_theme].""" _FOLLOW_SYSTEM_THEME = object() """Sentinel default for `ThemeContext`: resolve from the color scheme.""" ThemeContext: Context = create_context(_FOLLOW_SYSTEM_THEME) """Theme context that follows the system color scheme by default. Without a provider, [`use_theme`][pythonnative.use_theme] resolves to [`DEFAULT_LIGHT_THEME`][pythonnative.style.DEFAULT_LIGHT_THEME] or [`DEFAULT_DARK_THEME`][pythonnative.style.DEFAULT_DARK_THEME] based on the current appearance. Wrap a subtree in [`ThemeContext.Provider(my_theme, ...)`][pythonnative.hooks.Context.Provider] to pin an explicit theme for that subtree, then read it inside descendants via [`use_theme`][pythonnative.use_theme] (or [`use_context(ThemeContext)`][pythonnative.use_context]). """ def default_theme(scheme: str) -> Theme: """Return the built-in [`Theme`][pythonnative.Theme] for ``scheme`` (``"light"`` / ``"dark"``).""" return DEFAULT_DARK_THEME if scheme == "dark" else DEFAULT_LIGHT_THEME def use_theme() -> Theme: """Return the active [`Theme`][pythonnative.Theme], following the system appearance by default. If an ancestor mounted a ``ThemeContext.Provider(...)``, that theme is returned as-is. Otherwise the built-in light or dark theme is selected from the effective color scheme (via [`use_color_scheme`][pythonnative.use_color_scheme], so the component re-renders when the system appearance flips). Returns: The active theme. Raises: RuntimeError: If called outside a `@component` function. Example: ```python import pythonnative as pn @pn.component def Card(): theme = pn.use_theme() return pn.View( pn.Text("Hello", style=pn.style(color=theme.text_color)), style=pn.style(background_color=theme.surface_color), ) ``` """ scheme = use_color_scheme() theme = use_context(ThemeContext) if theme is _FOLLOW_SYSTEM_THEME: return default_theme(scheme) if not isinstance(theme, Theme): raise TypeError(f"ThemeContext.Provider expects a pn.Theme, got {type(theme).__name__}: {theme!r}") return theme __all__ = [ "AlignItems", "AlignSelf", "AutoCapitalize", "Color", "DEFAULT_DARK_THEME", "DEFAULT_LIGHT_THEME", "Dimension", "Display", "EdgeInsets", "EdgeValue", "FlexDirection", "FontWeight", "JustifyContent", "KeyboardType", "MarginValue", "Overflow", "PointerEvents", "Position", "ReturnKeyType", "ScaleType", "ShadowOffset", "ShadowOffsetValue", "Style", "StyleProp", "StyleSheet", "TextAlign", "TextDecoration", "TextTransform", "Theme", "ThemeContext", "TransformEntry", "TransformRotate", "TransformScale", "TransformScaleX", "TransformScaleY", "TransformSpec", "TransformTranslate", "default_theme", "resolve_style", "style", "use_theme", "validate_style_keys", ]

Back | FazBrowse Home | New Git URL