70 lines
2.2 KiB
Python
70 lines
2.2 KiB
Python
"""Reusable hypothesis strategies for CSS selectors."""
|
|
|
|
from typing import cast
|
|
|
|
from hypothesis import strategies as st
|
|
|
|
# CSS identifier (used for tag, class, ID names)
|
|
css_ident = st.from_regex(r"[_a-zA-Z][_a-zA-Z0-9-]*", fullmatch=True)
|
|
|
|
css_tag = css_ident
|
|
css_class = css_ident.map(lambda x: f".{x}")
|
|
css_class_multi = st.lists(css_class, min_size=1).map("".join)
|
|
css_id = css_ident.map(lambda x: f"#{x}")
|
|
|
|
# Any amount of whitespace, including none (spaces only)
|
|
_whitespace = st.from_regex(r" *", fullmatch=True)
|
|
|
|
|
|
def simple_selector() -> st.SearchStrategy[str]:
|
|
"""Returns a strategy for simple seletors: tag, .class, #id (all optional, at least one)."""
|
|
|
|
# Optional tag + (one or more classes) + optional id, at least one item present
|
|
parts = st.tuples(
|
|
st.none() | css_tag,
|
|
st.none() | css_class_multi,
|
|
st.none() | css_id,
|
|
).filter(lambda parts: any(parts))
|
|
|
|
return parts.map(lambda parts: "".join(p for p in parts if p))
|
|
|
|
|
|
# nth-child pseudo-classes
|
|
nth_child_suffix = st.from_regex(r":nth-child\([1-9][0-9]*\)", fullmatch=True)
|
|
_specific_child_pseudo_classes = st.sampled_from([":first-child", ":last-child"])
|
|
any_nth_child_suffix = st.one_of(nth_child_suffix, _specific_child_pseudo_classes)
|
|
|
|
|
|
def not_selector(inner: st.SearchStrategy[str]) -> st.SearchStrategy[str]:
|
|
return st.tuples(st.just(":not("), _whitespace, inner, _whitespace, st.just(")")).map("".join)
|
|
|
|
|
|
def pseudo_suffixes(inner: st.SearchStrategy[str]) -> st.SearchStrategy[str]:
|
|
"""Generate a single random pseudo-class suffix."""
|
|
return st.one_of(
|
|
not_selector(inner),
|
|
any_nth_child_suffix,
|
|
)
|
|
|
|
|
|
combinator = st.tuples(_whitespace, st.sampled_from([">", " ", "+", ",", "~"]), _whitespace).map("".join)
|
|
|
|
selector = st.recursive(
|
|
# Base: simple selector + pseudo-classes
|
|
base=simple_selector(),
|
|
extend=lambda s: st.one_of(
|
|
# Add combinator + selector sequence
|
|
cast(
|
|
"st.SearchStrategy[str]",
|
|
st.builds(
|
|
lambda left, comb, right: left + comb + right,
|
|
s,
|
|
combinator,
|
|
s,
|
|
),
|
|
),
|
|
# Apply pseudo-suffix
|
|
st.tuples(s, pseudo_suffixes(s)).map("".join),
|
|
),
|
|
max_leaves=10,
|
|
)
|