from contextlib import contextmanager
from capybara.selector.expression_filter import ExpressionFilter
from capybara.selector.filter_set import filter_sets
from capybara.selector.node_filter import NodeFilter
from capybara.utils import setter_decorator
selectors = {}
# Dict[str, Selector]: A dictionary of :class:`Selector` objects keyed by name. """
[docs]class Selector(object):
"""
A callable object used for selecting elements in a document or node.
Args:
name (str): The name of the selector.
label (str, optional): The label to use when describing this selector.
descriptions (List[Callable[[Dict[str, Any]], str]]): Functions that build a description of
the given filter options.
css (Callable[[str], str], optional): A function to generate a CSS selector given a locator
string.
xpath (Callable[[str], str], optional): A function to generate an XPath query given a
locator string.
filters (Dict[str, AbstractFilter]): A dictionary of filters this selector should use to
identify matching elements. Defaults to {}.
"""
def __init__(self, name, label=None, descriptions=None, css=None, xpath=None, filters=None):
self.name = name
self.label = label
self.descriptions = descriptions or []
self.css = css
self.xpath = xpath
self.format = "xpath" if xpath else "css"
self.filters = filters or {}
def __call__(self, locator):
assert self.format, "selector has no format"
return getattr(self, self.format)(locator)
[docs] def description(self, options):
"""
Returns a description of the given filter options relevant to this selector.
Args:
options (Dict[str, Any]): The filter options to describe.
Returns:
str: A description of the filter options.
"""
return "".join([describe(options) for describe in self.descriptions])
@property
def expression_filters(self):
""" Dict[str, ExpressionFilter]: Returns the expression filters for this selector. """
return {
name: filter for name, filter in iter(self.filters.items())
if isinstance(filter, ExpressionFilter)}
@property
def node_filters(self):
""" Dict[str, NodeFilter]: Returns the node filters for this selector. """
return {
name: filter for name, filter in iter(self.filters.items())
if isinstance(filter, NodeFilter)}
[docs]class SelectorFactory(object):
"""
A factory for configuring and building :class:`Selector` instances.
Args:
name (str): The name of the selector.
"""
def __init__(self, name):
self.name = name
self.label = None
self.descriptions = []
self.func = None
self.format = None
self.filters = {}
[docs] def describe(self, func):
"""
Decorates a function that builds a description of some selector options.
Args:
func (Callable[[Dict[str, Any]], str]): The description builder function.
"""
self.descriptions.append(func)
@setter_decorator
def css(self, func):
"""
Sets the given function as the CSS selector generation function.
Args:
func (Callable[[str], str]): The CSS selector generation function.
"""
self.func = func
self.format = "css"
@setter_decorator
def xpath(self, func):
"""
Sets the given function as the XPath query generation function.
Args:
func (Callable[[str], str]): The XPath query generation function.
"""
self.func = func
self.format = "xpath"
[docs] def expression_filter(self, name, **kwargs):
"""
Returns a decorator function for adding an expression filter.
Args:
name (str): The name of the filter.
**kwargs: Variable keyword arguments for the filter.
Returns:
Callable[[Callable[[AbstractExpression, Any], AbstractExpression]]]: A decorator
function for adding an expression filter.
"""
def decorator(func):
self.filters[name] = ExpressionFilter(name, func, **kwargs)
return decorator
[docs] def node_filter(self, name, **kwargs):
"""
Returns a decorator function for adding a node filter.
Args:
name (str): The name of the filter.
**kwargs: Variable keyword arguments for the filter.
Returns:
Callable[[Callable[[Element, Any], bool]]]: A decorator function for adding a node
filter.
"""
def decorator(func):
self.filters[name] = NodeFilter(name, func, **kwargs)
return decorator
[docs] def filter_set(self, name):
"""
Adds filters from a particular global :class:`FilterSet`.
Args:
name (str): The name of the set whose filters should be added.
"""
filter_set = filter_sets[name]
for name, filter in iter(filter_set.filters.items()):
self.filters[name] = filter
self.descriptions += filter_set.descriptions
[docs] def build_selector(self):
""" Selector: Returns a new :class:`Selector` instance with the current configuration. """
kwargs = {
'label': self.label,
'descriptions': self.descriptions,
'filters': self.filters}
if self.format == "xpath":
kwargs['xpath'] = self.func
if self.format == "css":
kwargs['css'] = self.func
return Selector(self.name, **kwargs)
[docs]@contextmanager
def add_selector(name):
"""
Builds and registers a :class:`Selector` object with the given name and configuration.
Args:
name (str): The name of the selector.
Yields:
SelectorFactory: The factory that will build the :class:`Selector`.
"""
factory = SelectorFactory(name)
yield factory
selectors[name] = factory.build_selector()
[docs]def remove_selector(name):
"""
Unregisters selector with the given name.
Args:
name (str): The name of the selector.
"""
selectors.pop(name, None)