Skip to content

Commit

Permalink
Merge pull request #214 from Textualize/time-no-scalar
Browse files Browse the repository at this point in the history
Splitting out parsing of durations into new token types, avoiding Scalar
  • Loading branch information
willmcgugan authored Jan 19, 2022
2 parents 3af8c5a + c462beb commit 185788b
Show file tree
Hide file tree
Showing 11 changed files with 251 additions and 65 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ repos:
rev: 21.8b0
hooks:
- id: black
exclude: ^tests/
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ typecheck:
format:
black src
format-check:
black --check .
black --check src
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@ classifiers = [
"Programming Language :: Python :: 3.10",
]


[tool.poetry.dependencies]
python = "^3.7"
rich = "^10.12.0"
#rich = {git = "[email protected]:willmcgugan/rich", rev = "link-id"}
typing-extensions = { version = "^3.10.0", python = "<3.8" }

[tool.poetry.dev-dependencies]

pytest = "^6.2.3"
black = "^21.11b1"
mypy = "^0.910"
Expand All @@ -35,6 +33,9 @@ mkdocstrings = "^0.15.2"
mkdocs-material = "^7.1.10"
pre-commit = "^2.13.0"

[tool.black]
includes = "src"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
47 changes: 47 additions & 0 deletions src/textual/_duration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import re

_match_duration = re.compile(r"^(-?\d+\.?\d*)(s|ms)$").match


class DurationError(Exception):
"""
Exception indicating a general issue with a CSS duration.
"""


class DurationParseError(DurationError):
"""
Indicates a malformed duration string that could not be parsed.
"""


def _duration_as_seconds(duration: str) -> float:
"""
Args:
duration (str): A string of the form ``"2s"`` or ``"300ms"``, representing 2 seconds and
300 milliseconds respectively. If no unit is supplied, e.g. ``"2"``, then the duration is
assumed to be in seconds.
Raises:
DurationParseError: If the argument ``duration`` is not a valid duration string.
Returns:
float: The duration in seconds.
"""
match = _match_duration(duration)

if match:
value, unit_name = match.groups()
value = float(value)
if unit_name == "ms":
duration_secs = value / 1000
else:
duration_secs = value
else:
try:
duration_secs = float(duration)
except ValueError:
raise DurationParseError(
f"{duration!r} is not a valid duration."
) from ValueError

return duration_secs
1 change: 0 additions & 1 deletion src/textual/css/_style_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ def __get__(
def __set__(
self, obj: Styles, value: float | Scalar | str | None
) -> float | Scalar | str | None:
new_value: Scalar | None = None
if value is None:
new_value = None
elif isinstance(value, float):
Expand Down
59 changes: 29 additions & 30 deletions src/textual/css/_styles_builder.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
"""
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
Expand All @@ -17,6 +9,7 @@
from .constants import VALID_BORDER, VALID_EDGE, VALID_DISPLAY, VALID_VISIBILITY
from .errors import DeclarationError
from ._error_tools import friendly_list
from .._duration import _duration_as_seconds
from .._easing import EASING
from ..geometry import Spacing, SpacingDimensions
from .model import Declaration
Expand All @@ -28,6 +21,11 @@


class StylesBuilder:
"""
The StylesBuilder object takes tokens parsed from the CSS and converts
to the appropriate internal types.
"""

def __init__(self) -> None:
self.styles = Styles()

Expand Down Expand Up @@ -240,14 +238,21 @@ def process_offset(self, name: str, tokens: list[Token], important: bool) -> Non
if not tokens:
return
if len(tokens) != 2:
self.error(name, tokens[0], "expected two numbers in declaration")
self.error(
name, tokens[0], "expected two scalars or numbers in declaration"
)
else:
token1, token2 = tokens

if token1.name != "scalar":
self.error(name, token1, f"expected a scalar; found {token1.value!r}")
if token2.name != "scalar":
self.error(name, token2, f"expected a scalar; found {token1.value!r}")
if token1.name not in ("scalar", "number"):
self.error(
name, token1, f"expected a scalar or number; found {token1.value!r}"
)
if token2.name not in ("scalar", "number"):
self.error(
name, token2, f"expected a scalar or number; found {token2.value!r}"
)

scalar_x = Scalar.parse(token1.value, Unit.WIDTH)
scalar_y = Scalar.parse(token2.value, Unit.HEIGHT)
self.styles._rule_offset = ScalarOffset(scalar_x, scalar_y)
Expand All @@ -259,7 +264,7 @@ def process_offset_x(self, name: str, tokens: list[Token], important: bool) -> N
self.error(name, tokens[0], f"expected a single number")
else:
token = tokens[0]
if token.name != "scalar":
if token.name not in ("scalar", "number"):
self.error(name, token, f"expected a scalar; found {token.value!r}")
x = Scalar.parse(token.value, Unit.WIDTH)
y = self.styles.offset.y
Expand All @@ -272,7 +277,7 @@ def process_offset_y(self, name: str, tokens: list[Token], important: bool) -> N
self.error(name, tokens[0], f"expected a single number")
else:
token = tokens[0]
if token.name != "scalar":
if token.name not in ("scalar", "number"):
self.error(name, token, f"expected a scalar; found {token.value!r}")
y = Scalar.parse(token.value, Unit.HEIGHT)
x = self.styles.offset.x
Expand Down Expand Up @@ -394,15 +399,8 @@ def process_transition(
) -> None:
transitions: dict[str, Transition] = {}

css_property = ""
duration = 1.0
easing = "linear"
delay = 0.0

iter_tokens = iter(tokens)

def make_groups() -> Iterable[list[Token]]:
"""Batch tokens in to comma-separated groups."""
"""Batch tokens into comma-separated groups."""
group: list[Token] = []
for token in tokens:
if token.name == "comma":
Expand All @@ -414,6 +412,7 @@ def make_groups() -> Iterable[list[Token]]:
if group:
yield group

valid_duration_token_names = ("duration", "number")
for tokens in make_groups():
css_property = ""
duration = 1.0
Expand All @@ -425,13 +424,13 @@ def make_groups() -> Iterable[list[Token]]:
token = next(iter_tokens)
if token.name != "token":
self.error(name, token, "expected property")
css_property = token.value

css_property = token.value
token = next(iter_tokens)
if token.name != "scalar":
self.error(name, token, "expected time")
if token.name not in valid_duration_token_names:
self.error(name, token, "expected duration or number")
try:
duration = Scalar.parse(token.value).resolve_time()
duration = _duration_as_seconds(token.value)
except ScalarError as error:
self.error(name, token, str(error))

Expand All @@ -448,10 +447,10 @@ def make_groups() -> Iterable[list[Token]]:
easing = token.value

token = next(iter_tokens)
if token.name != "scalar":
self.error(name, token, "expected time")
if token.name not in valid_duration_token_names:
self.error(name, token, "expected duration or number")
try:
delay = Scalar.parse(token.value).resolve_time()
delay = _duration_as_seconds(token.value)
except ScalarError as error:
self.error(name, token, str(error))
except StopIteration:
Expand Down
30 changes: 20 additions & 10 deletions src/textual/css/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:

def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:

rule_set = RuleSet()

get_selector = SELECTOR_MAP.get
combinator: CombinatorType | None = CombinatorType.DESCENDENT
selectors: list[Selector] = []
Expand Down Expand Up @@ -187,8 +185,8 @@ def parse_declarations(css: str, path: str) -> Styles:
try:
styles_builder.add_declaration(declaration)
except DeclarationError as error:
raise
errors.append((error.token, error.message))
raise
declaration = Declaration(token, "")
declaration.name = token.value.rstrip(":")
elif token_name == "declaration_set_end":
Expand All @@ -201,8 +199,8 @@ def parse_declarations(css: str, path: str) -> Styles:
try:
styles_builder.add_declaration(declaration)
except DeclarationError as error:
raise
errors.append((error.token, error.message))
raise

return styles_builder.styles

Expand Down Expand Up @@ -257,9 +255,21 @@ def parse(css: str, path: str) -> Iterable[RuleSet]:
if __name__ == "__main__":
print(parse_selectors("Foo > Bar.baz { foo: bar"))

CSS = """
text: on red;
docksX: main=top;
"""

print(parse_declarations(CSS, "foo"))
css = """#something {
text: on red;
transition: offset 5.51s in_out_cubic;
offset-x: 100%;
}
"""

from textual.css.stylesheet import Stylesheet, StylesheetParseError
from rich.console import Console

console = Console()
stylesheet = Stylesheet()
try:
stylesheet.parse(css)
except StylesheetParseError as e:
console.print(e.errors)
print(stylesheet)
print(stylesheet.css)
15 changes: 2 additions & 13 deletions src/textual/css/scalar.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import rich.repr

from textual.css.tokenizer import Token
from ..geometry import Offset


Expand All @@ -30,8 +31,6 @@ class Unit(Enum):
HEIGHT = 5
VIEW_WIDTH = 6
VIEW_HEIGHT = 7
MILLISECONDS = 8
SECONDS = 9


UNIT_SYMBOL = {
Expand All @@ -42,13 +41,11 @@ class Unit(Enum):
Unit.HEIGHT: "h",
Unit.VIEW_WIDTH: "vw",
Unit.VIEW_HEIGHT: "vh",
Unit.MILLISECONDS: "ms",
Unit.SECONDS: "s",
}

SYMBOL_UNIT = {v: k for k, v in UNIT_SYMBOL.items()}

_MATCH_SCALAR = re.compile(r"^(\-?\d+\.?\d*)(fr|%|w|h|vw|vh|s|ms)?$").match
_MATCH_SCALAR = re.compile(r"^(-?\d+\.?\d*)(fr|%|w|h|vw|vh)?$").match


RESOLVE_MAP = {
Expand Down Expand Up @@ -142,14 +139,6 @@ def resolve_dimension(
except KeyError:
raise ScalarResolveError(f"expected dimensions; found {str(self)!r}")

def resolve_time(self) -> float:
value, unit, _ = self
if unit == Unit.MILLISECONDS:
return value / 1000.0
elif unit == Unit.SECONDS:
return value
raise ScalarResolveError(f"expected time; found {str(self)!r}")


@rich.repr.auto(angular=True)
class ScalarOffset(NamedTuple):
Expand Down
7 changes: 3 additions & 4 deletions src/textual/css/stylesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
from collections import defaultdict
from operator import itemgetter
import os
from typing import Iterable, TYPE_CHECKING
from typing import Iterable

from rich.console import RenderableType
import rich.repr
from rich.highlighter import ReprHighlighter
from rich.panel import Panel
Expand Down Expand Up @@ -86,11 +85,11 @@ def read(self, filename: str) -> None:
css = css_file.read()
path = os.path.abspath(filename)
except Exception as error:
raise StylesheetError(f"unable to read {filename!r}; {error}") from None
raise StylesheetError(f"unable to read {filename!r}; {error}")
try:
rules = list(parse(css, path))
except Exception as error:
raise StylesheetError(f"failed to parse {filename!r}; {error}") from None
raise StylesheetError(f"failed to parse {filename!r}; {error}")
self.rules.extend(rules)

def parse(self, css: str, *, path: str = "") -> None:
Expand Down
20 changes: 16 additions & 4 deletions src/textual/css/tokenize.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from __future__ import annotations

import pprint
import re
from typing import Iterable

from rich import print

from .tokenizer import Expect, Tokenizer, Token
from textual.css.tokenizer import Expect, Tokenizer, Token


expect_selector = Expect(
Expand Down Expand Up @@ -51,7 +51,9 @@
declaration_end=r"\n|;",
whitespace=r"\s+",
comment_start=r"\/\*",
scalar=r"\-?\d+\.?\d*(?:fr|%|w|h|vw|vh|s|ms)?",
scalar=r"\-?\d+\.?\d*(?:fr|%|w|h|vw|vh)",
duration=r"\d+\.?\d*(?:ms|s)",
number=r"\-?\d+\.?\d*",
color=r"\#[0-9a-fA-F]{6}|color\([0-9]{1,3}\)|rgb\(\d{1,3}\,\s?\d{1,3}\,\s?\d{1,3}\)",
key_value=r"[a-zA-Z_-][a-zA-Z0-9_-]*=[0-9a-zA-Z_\-\/]+",
token="[a-zA-Z_-]+",
Expand Down Expand Up @@ -124,3 +126,13 @@ class DeclarationTokenizerState(TokenizerState):
# break
# expect = get_state(name, expect)
# yield token

if __name__ == "__main__":
css = """#something {
text: on red;
offset-x: 10;
}
"""
# transition: offset 500 in_out_cubic;
tokens = tokenize(css, __name__)
pprint.pp(list(tokens))
Loading

0 comments on commit 185788b

Please sign in to comment.