"""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), # Optionally add classes / id after the pseudo-suffix st.one_of( s, # no append, keep as-is st.tuples( s, st.lists(st.one_of(css_class, css_id), min_size=1).map("".join), ).map("".join), ), ), max_leaves=10, )