"""
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 Cookie(Facet):
"""
enforces the existence (and optionally the value) of a cookie.
:param str name: name of the cookie
:param dict value: (optional) dict (or callable) to validate the value of
the cookie.
: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__ = ["name"]
__OPTIONS__ = {"value": None}
def evaluate(self, driver):
cookie_value = driver.get_cookie(self.arguments["name"])
if cookie_value and self.options["value"]:
if callable(self.options["value"]):
assert self.options["value"](cookie_value)
else:
assert cookie_value == self.options["value"]
else:
assert cookie_value is not None, (
"cookie %s does not exist" % self.arguments["name"]
)
[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