Source code for holmium.core.facets

"""
implementation of facet bases and builtin facets
"""

import inspect
import re
import weakref
from abc import ABCMeta, abstractmethod

from .logger import log


[docs]class Facet(metaclass=ABCMeta): """ base class to implement an attribute of a page """ __ARGS__ = [] __OPTIONS__ = {} __ALLOW_MULTIPLE__ = True def __init__(self, required=True, debug=False, **kwargs): self.arguments = {} self.options = {} self.required = required self.debug = debug self._parent_class = None for arg in self.__ARGS__: if arg not in kwargs.keys(): raise AttributeError( "{} is a required argument for {}".format( arg, self.__class__.__name__ ) ) else: self.arguments[arg] = kwargs[arg] kwargs.pop(arg) for arg in self.__OPTIONS__: if arg in kwargs.keys(): self.options[arg] = kwargs[arg] kwargs.pop(arg) else: self.options[arg] = self.__OPTIONS__[arg] if kwargs: raise AttributeError( "unknown argument(s) to %s (%s)" % (self.__class__.__name__, ",".join(kwargs.keys())) )
[docs] def register(self, obj): """ registers a :class:`Facet` on an object :param holmium.core.facets.Faceted obj: the object to register the facet on. """ if inspect.isclass(obj): obj.get_class_facets().append(self) self.parent_class = obj else: obj.get_instance_facets().append(self) self.parent_class = obj.__class__
def __call__(self, obj): self.register(obj) return obj
[docs] def get_name(self): """ returns the class name of the facet """ return self.__class__.__name__
@property def parent_class(self): """ returns the parent class """ return self._parent_class() @parent_class.setter def parent_class(self, parent): """ sets the parent class """ self._parent_class = weakref.ref(parent)
[docs] def get_parent_name(self): """ returns the class name of the parent """ return (self.parent_class and self.parent_class.__name__) or None
[docs] @abstractmethod def evaluate(self, driver): """ evaluate whether this facet holds true. Raise an Exception if not. :param selenium.webdriver.remote.webdriver.WebDriver driver: the webdriver """
[docs]class FacetError(Exception): """ exception raised when a facet has an error or can't complete :param holmium.core.facets.Facet facet: the facet that failed to evaluate :param exceptions.Exception exc: the inner exception that caused the failure """ def __init__(self, facet, exc=None): self.message = "{} failed to exhibit facet {}".format( facet.get_parent_name(), facet.get_name(), ) if exc: self.message += " with error %s" % exc super().__init__(self.message)
[docs]class FacetCollection(list): """ utility collection class for pageobjects to encapsulate facets """ def __init__(self, *a): super().__init__(*a) @property def type_map(self): """ view on the list to help with figuring out if a facet of the same type already exists """ type_mmap = {} for item in self: type_mmap.setdefault(type(item), []).append(item) return type_mmap
[docs] def append(self, item): """ overridden add method to pop the last item if its type does not support multiple facets on the same object. """ if type(item) in self.type_map and not type(item).__ALLOW_MULTIPLE__: self.remove(self.type_map[type(item)].pop()) if item not in self: super().append(item)
[docs] def evaluate_all(self, driver): """ iterate over all registered :class:`Facet` objects and validate them :param selenium.webdriver.remote.webdriver.WebDriver driver: the webdriver """ for facet in self: try: facet.evaluate(driver) # pylint: disable=broad-except except Exception as _: if facet.debug: log.warn(FacetError(facet, _)) elif facet.required: raise FacetError(facet, _)
class CopyOnCreateFacetCollectionMeta(ABCMeta): """ makes a new copy of any :class:`FacetCollection` instances upon creating the class. This is to ensure that different derived classes of :class:`Page` do not clobber the class facets of the base class. """ def __init__(cls, *args): super().__init__(*args) visited = {} for superclass in cls.__mro__: for key, value in vars(superclass).items(): if isinstance(value, (FacetCollection)): visited.setdefault(key, FacetCollection()) for facet in value: visited[key].append(facet) setattr(cls, key, FacetCollection(visited[key]))
[docs]class Faceted(metaclass=CopyOnCreateFacetCollectionMeta): """ mixin for objects that want to have facets registered on them. """ def __init__(self): self.instance_facets = FacetCollection() super().__init__()
[docs] @classmethod def get_class_facets(cls): """ returns the facets registered on the class (presumably via a decorator) """ if not hasattr(cls, "class_facets"): cls.class_facets = FacetCollection() return cls.class_facets
[docs] def get_instance_facets(self): """ returns the facets registered on the instance """ return object.__getattribute__(self, "instance_facets")
[docs] def evaluate(self): """ evaluates all registered facets (class & instance) """ from .pageobject import Page def safe_get(e): return object.__getattribute__(self, e) driver = Page.get_driver() instance_facets = safe_get("get_instance_facets")() class_facets = safe_get("get_class_facets")() class_facets.evaluate_all(driver) instance_facets.evaluate_all(driver)
[docs]class Defer(Facet): """ :param holmium.core.Page page: the page object that is expected to be deferred to :param function action: a callable that takes the page object instance as the first argument :param dict action_arguments: (optional) dictionary of arguments to pass to `action` :param bool debug: if True a failure to evaluate will not result in an exception, only a log warning :param bool required: if False a failure to evaluate will be treated as a noop. """ __ARGS__ = ["page", "action"] __OPTIONS__ = {"action_arguments": {}} def evaluate(self, driver): page_cls = self.arguments["page"] page = page_cls(driver) return self.arguments["action"](page, **self.options["action_arguments"])
[docs]class Title(Facet): """ enforces the title of the current page. :param str title: a regular expression to match the title. :param bool debug: if True a failure to evaluate will not result in an exception, only a log warning :param bool required: if False a failure to evaluate will be treated as a noop. """ __ARGS__ = ["title"] __ALLOW_MULTIPLE__ = False def evaluate(self, driver): assert re.compile(self.arguments["title"]).match(driver.title), ( "title did not match %s" % self.arguments["title"] )
[docs]class Strict(Facet): """ enforces that every element declared in the :class:`Page` or :class:`Section` be present. :param bool debug: if True a failure to evaluate will not result in an exception, only a log warning :param bool required: if False a failure to evaluate will be treated as a noop. """ def evaluate(self, driver): pass # pragma: no cover def __call__(self, obj): from .pageobject import ElementGetter for element in inspect.getmembers(obj): if isinstance(element[1], ElementGetter): element[1].is_facet = True element[1].is_debug_facet = self.debug return obj
class ElementFacet(Facet): """ utility trait used when validating an :class:`holmium.core.pageobject.ElementGetter` subclass """ def __init__(self, element, element_name, **kwargs): self.element_name = element_name self.element = element super().__init__(required=True, **kwargs) def evaluate(self, driver): assert self.element.__get__( self.parent_class, self.parent_class ), "No such element" def get_name(self): return self.element_name # pylint: disable=invalid-name cookie = Cookie strict = Strict defer = Defer title = Title