docstrings and tidy

This commit is contained in:
Will McGugan
2022-01-02 15:22:43 +00:00
parent d4b9972e2f
commit 2635f58e7c
11 changed files with 198 additions and 99 deletions

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
from rich.segment import Segment, SegmentLines
from rich.style import Style
from rich.style import Style, StyleType
from .css.types import EdgeStyle

View File

@@ -1,3 +1,12 @@
"""
Style properties are descriptors which allow the Styles object to accept different types when
setting attributes. This gives the developer more freedom in how to express style information.
Descriptors also play nicely with Mypy, which is aware that attributes can have different types
when setting and getting.
"""
from __future__ import annotations
from typing import Iterable, NamedTuple, Sequence, TYPE_CHECKING
@@ -125,6 +134,20 @@ class Edges(NamedTuple):
if left[0]:
yield "left", left
def spacing(self) -> tuple[int, int, int, int]:
"""Get spacing created by borders.
Returns:
tuple[int, int, int, int]: Spacing for top, right, bottom, and left.
"""
top, right, bottom, left = self
return (
1 if top[0] else 0,
1 if right[0] else 0,
1 if bottom[0] else 0,
1 if left[0] else 0,
)
class BorderProperty:
def __set_name__(self, owner: Styles, name: str) -> None:

View File

@@ -1,16 +1,24 @@
"""
The StylesBuilder object takes tokens parsed from the CSS and converts
to the appropriate internal types.
"""
from __future__ import annotations
from typing import cast, Iterable, NoReturn
import rich.repr
from rich.color import ANSI_COLOR_NAMES, Color
from rich.color import Color
from rich.style import Style
from .constants import VALID_BORDER, VALID_EDGE, VALID_DISPLAY, VALID_VISIBILITY
from .errors import DeclarationError, StyleValueError
from .errors import DeclarationError
from ._error_tools import friendly_list
from .._easing import EASING
from ..geometry import Offset, Spacing, SpacingDimensions
from ..geometry import Spacing, SpacingDimensions
from .model import Declaration
from .scalar import Scalar, ScalarOffset, Unit, ScalarError
from .styles import DockGroup, Styles

View File

@@ -1,45 +0,0 @@
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Space:
top: int = 0
right: int = 0
bottom: int = 0
left: int = 0
def __str__(self) -> str:
return f"{self.top}, {self.right}, {self.bottom}, {self.left}"
@dataclass(frozen=True)
class Edge:
line: str = "none"
style: str = "default"
@dataclass(frozen=True)
class Border:
top: Edge = field(default_factory=Edge)
right: Edge = field(default_factory=Edge)
bottom: Edge = field(default_factory=Edge)
left: Edge = field(default_factory=Edge)
@dataclass
class Box:
padding: Space = field(default_factory=Space)
margin: Space = field(default_factory=Space)
border: Border = field(default_factory=Border)
outline: Border = field(default_factory=Border)
dispay: bool = True
visible: bool = True
text: str = ""
opacity: float = 1.0
if __name__ == "__main__":
from rich import print
box = Box()
print(box)

View File

@@ -9,6 +9,15 @@ if TYPE_CHECKING:
def match(selector_sets: Iterable[SelectorSet], node: DOMNode) -> bool:
"""Check if a given selector matches any of the given selector sets.
Args:
selector_sets (Iterable[SelectorSet]): Iterable of selector sets.
node (DOMNode): DOM node.
Returns:
bool: True if the node matches the selector, otherwise False.
"""
return any(
_check_selectors(selector_set.selectors, node) for selector_set in selector_sets
)

View File

@@ -1,3 +1,16 @@
"""
A DOMQuery is a set of DOM nodes associated with a given CSS selector.
This set of nodes may be further filtered with the filter method. Additional methods apply
actions to the nodes in the query.
If this sounds like JQuery, a (once) popular JS library, it is no coincidence.
DOMQuery objects are typically created by Widget.filter method.
"""
from __future__ import annotations
@@ -38,6 +51,7 @@ class DOMQuery:
return len(self._nodes)
def __bool__(self) -> bool:
"""True if non-empty, otherwise False."""
return bool(self._nodes)
def __iter__(self) -> Iterator[DOMNode]:
@@ -47,22 +61,39 @@ class DOMQuery:
yield self._nodes
def filter(self, selector: str) -> DOMQuery:
"""Filter this set by the given CSS selector.
Args:
selector (str): A CSS selector.
Returns:
DOMQuery: New DOM Query.
"""
selector_set = parse_selectors(selector)
query = DOMQuery()
query._nodes = [_node for _node in self._nodes if match(selector_set, _node)]
return query
def first(self) -> DOMNode:
"""Get the first matched node.
Returns:
DOMNode: A DOM Node.
"""
# TODO: Better response to empty query than an IndexError
return self._nodes[0]
def add_class(self, *class_names: str) -> None:
"""Add the given class name(s) to nodes."""
for node in self._nodes:
node.add_class(*class_names)
def remove_class(self, *class_names: str) -> None:
"""Remove the given class names from the nodes."""
for node in self._nodes:
node.remove_class(*class_names)
def toggle_class(self, *class_names: str) -> None:
"""Toggle the given class names from matched nodes."""
for node in self._nodes:
node.toggle_class(*class_names)

View File

@@ -154,6 +154,16 @@ class Styles:
"min_height",
}
@property
def gutter(self) -> Spacing:
"""Get the gutter (additional space reserved for margin / padding / border).
Returns:
Spacing: [description]
"""
gutter = self.margin + self.padding + self.border.spacing
return gutter
@classmethod
@lru_cache(maxsize=1024)
def parse(cls, css: str, path: str) -> Styles:

View File

@@ -50,6 +50,14 @@ class DOMNode(MessagePump):
@property
def parent(self) -> DOMNode:
"""Get the parent node.
Raises:
NoParent: If this is the root node.
Returns:
DOMNode: The node which is the direct parent of this node.
"""
if self._parent is None:
raise NoParent(f"{self} has no parent")
assert isinstance(self._parent, DOMNode)
@@ -57,10 +65,24 @@ class DOMNode(MessagePump):
@property
def id(self) -> str | None:
"""The ID of this node, or None if the node has no ID.
Returns:
(str | None): A Node ID or None.
"""
return self._id
@id.setter
def id(self, new_id: str) -> str:
"""Sets the ID (may only be done once).
Args:
new_id (str): ID for this node.
Raises:
ValueError: If the ID has already been set.
"""
if self._id is not None:
raise ValueError(
"Node 'id' attribute may not be changed once set (current id={self._id!r})"
@@ -83,10 +105,20 @@ class DOMNode(MessagePump):
@property
def css_type(self) -> str:
"""Gets the CSS type, used by the CSS.
Returns:
str: A type used in CSS (lower cased class name).
"""
return self.__class__.__name__.lower()
@property
def css_path(self) -> list[DOMNode]:
"""A list of nodes from the root to this node, forming a "path".
Returns:
list[DOMNode]: List of Nodes, starting with the root and ending with this node.
"""
result: list[DOMNode] = [self]
append = result.append
@@ -137,6 +169,11 @@ class DOMNode(MessagePump):
@property
def tree(self) -> Tree:
"""Get a Rich tree object which will recursively render the structure of the node tree.
Returns:
Tree: A Rich object which may be printed.
"""
highlighter = ReprHighlighter()
tree = Tree(highlighter(repr(self)))
@@ -163,6 +200,11 @@ class DOMNode(MessagePump):
pass
def add_child(self, node: DOMNode) -> None:
"""Add a new child node.
Args:
node (DOMNode): A DOM node.
"""
self.children._append(node)
node.set_parent(self)

View File

@@ -507,5 +507,14 @@ class Spacing(NamedTuple):
return cls(top, right, bottom, left)
raise ValueError(f"1, 2 or 4 integers required for spacing; {len(pad)} given")
def __add__(self, other: object) -> Spacing:
if isinstance(other, tuple):
top1, right1, bottom1, left1 = self
top2, right2, bottom2, left2 = other
return Spacing(
top1 + top2, right1 + right2, bottom1 + bottom2, left1 + left2
)
return NotImplemented
NULL_OFFSET = Offset(0, 0)

View File

@@ -84,18 +84,12 @@ class View(Widget):
def scroll(self) -> Offset:
return Offset(self.scroll_x, self.scroll_y)
# def __rich_console__(
# self, console: Console, options: ConsoleOptions
# ) -> RenderResult:
# return
# yield
def __rich_repr__(self) -> rich.repr.Result:
yield "name", self.name
def __getitem__(self, widget_name: str) -> Widget:
def __getitem__(self, widget_id: str) -> Widget:
try:
return self.get_child(widget_name)
return self.get_child_by_id(widget_id)
except errors.MissingWidget as error:
raise KeyError(str(error))

View File

@@ -65,12 +65,12 @@ class Widget(DOMNode):
"""
def __init__(self, name: str | None = None, id: str | None = None) -> None:
# if name is None:
# class_name = self.__class__.__name__
# Widget._counts.setdefault(class_name, 0)
# Widget._counts[class_name] += 1
# _count = self._counts[class_name]
# name = f"{class_name}{_count}"
if name is None:
class_name = self.__class__.__name__
Widget._counts.setdefault(class_name, 0)
Widget._counts[class_name] += 1
_count = self._counts[class_name]
name = f"{class_name}{_count}"
self._size = Size(0, 0)
self._repaint_required = False
@@ -82,26 +82,6 @@ class Widget(DOMNode):
super().__init__(name=name, id=id)
# visible: Reactive[bool] = Reactive(True, layout=True)
layout_size: Reactive[int | None] = Reactive(None, layout=True)
layout_fraction: Reactive[int] = Reactive(1, layout=True)
layout_min_size: Reactive[int] = Reactive(1, layout=True)
# layout_offset_x: Reactive[float] = Reactive(0.0, layout=True)
# layout_offset_y: Reactive[float] = Reactive(0.0, layout=True)
# style: Reactive[str | None] = Reactive(None)
padding: Reactive[Spacing | None] = Reactive(None, layout=True)
margin: Reactive[Spacing | None] = Reactive(None, layout=True)
border: Reactive[str] = Reactive("none", layout=True)
border_style: Reactive[str] = Reactive("green")
border_title: Reactive[TextType] = Reactive("")
def validate_padding(self, padding: SpacingDimensions) -> Spacing:
return Spacing.unpack(padding)
def validate_margin(self, margin: SpacingDimensions) -> Spacing:
return Spacing.unpack(margin)
def __init_subclass__(cls, can_focus: bool = True) -> None:
super().__init_subclass__()
cls.can_focus = can_focus
@@ -117,16 +97,43 @@ class Widget(DOMNode):
renderable = self.render_styled()
return renderable
def get_child(self, name: str | None = None, id: str | None = None) -> Widget:
if name is not None:
for widget in self.children:
if widget.name == name:
return cast(Widget, widget)
if id is not None:
def get_child_by_id(self, id: str) -> Widget:
"""Get a child with a given id.
Args:
id (str): A Widget id.
Raises:
errors.MissingWidget: If the widget was not found.
Returns:
Widget: A child widget.
"""
for widget in self.children:
if widget.id == id:
return cast(Widget, widget)
raise errors.MissingWidget(f"Widget named {name!r} was not found in {self}")
raise errors.MissingWidget(f"Widget with id=={id!r} was not found in {self}")
def get_child_by_name(self, name: str) -> Widget:
"""Get a child widget with a given name.
Args:
name (str): A name. Defaults to None.
Raises:
errors.MissingWidget: If no Widget is found.
Returns:
Widget: A Widget with the given name.
"""
for widget in self.children:
if widget.name == name:
return cast(Widget, widget)
raise errors.MissingWidget(
f"Widget with name=={name!r} was not found in {self}"
)
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
watch(self, attribute_name, callback)
@@ -196,12 +203,12 @@ class Widget(DOMNode):
@property
def gutter(self) -> Spacing:
mt, mr, mb, bl = self.margin or (0, 0, 0, 0)
pt, pr, pb, pl = self.padding or (0, 0, 0, 0)
border = 1 if self.border else 0
gutter = Spacing(
mt + pt + border, mr + pr + border, mb + pb + border, bl + pl + border
)
"""Get additional space reserved by margin / padding / border.
Returns:
Spacing: [description]
"""
gutter = self.styles.gutter
return gutter
def on_style_change(self) -> None:
@@ -312,9 +319,20 @@ class Widget(DOMNode):
await self.app.set_focus(self)
async def capture_mouse(self, capture: bool = True) -> None:
"""Capture (or release) the mouse.
When captured, all mouse coordinates will go to this widget even when the pointer is not directly over the widget.
Args:
capture (bool, optional): True to capture or False to release. Defaults to True.
"""
await self.app.capture_mouse(self if capture else None)
async def release_mouse(self) -> None:
"""Release the mouse.
Mouse events will only be sent when the mouse is over the widget.
"""
await self.app.capture_mouse(None)
async def broker_event(self, event_name: str, event: events.Event) -> bool: