def __init__(self, parent, logger=None, **kwargs): Widget.__init__(self, parent, logger=logger) self.context = kwargs.pop('additional_context', {}) self.last_fill_data = None if not self.fill_strategy: if getattr(getattr(self.parent, 'fill_strategy', None), 'respect_parent', False): self.fill_strategy = self.parent.fill_strategy else: self.fill_strategy = DefaultFillViewStrategy()
def __init__(self, parent, logger=None, **kwargs): super().__init__(parent, logger=logger, **kwargs) self.context = kwargs.pop("additional_context", {}) self.last_fill_data = None if not self.fill_strategy: if getattr(getattr(self.parent, "fill_strategy", None), "respect_parent", False): self.fill_strategy = self.parent.fill_strategy else: self.fill_strategy = DefaultFillViewStrategy()
class View(Widget): """View is a kind of abstract widget that can hold another widgets. Remembers the order, so therefore it can function like a form with defined filling order. It looks like this: .. code-block:: python class Login(View): user = SomeInputWidget('user') password = SomeInputWidget('pass') login = SomeButtonWidget('Log In') def a_method(self): do_something() The view is usually instantiated with an instance of :py:class:`widgetastic.browser.Browser`, which will then enable resolving of all of the widgets defined. Args: parent: A parent :py:class:`View` or :py:class:`widgetastic.browser.Browser` additional_context: If the view needs some context, for example - you want to check that you are on the page of user XYZ but you can also be on the page for user FOO, then you shall use the ``additional_context`` to pass in required variables that will allow you to detect this. """ #: Skip this view in the element lookup hierarchy INDIRECT = False fill_strategy = None def __init__(self, parent, logger=None, **kwargs): Widget.__init__(self, parent, logger=logger) self.context = kwargs.pop('additional_context', {}) self.last_fill_data = None if not self.fill_strategy: if getattr(getattr(self.parent, 'fill_strategy', None), 'respect_parent', False): self.fill_strategy = self.parent.fill_strategy else: self.fill_strategy = DefaultFillViewStrategy() @staticmethod def nested(view_class): """Shortcut for :py:class:`WidgetDescriptor` Usage: .. code-block:: python class SomeView(View): some_widget = Widget() @View.nested class another_view(View): pass Why? The problem is counting things. When you are placing widgets themselves on a view, they handle counting themselves and just work. But when you are creating a nested view, that is a bit of a problem. The widgets are instantiated, whereas the views are placed in a class and wait for the :py:class:`ViewMetaclass` to pick them up, but that happens after all other widgets have been instantiated into the :py:class:`WidgetDescriptor`s, which has the consequence of things being out of order. By wrapping the class into the descriptor we do the job of :py:meth:`Widget.__new__` which creates the :py:class:`WidgetDescriptor` if not called with a :py:class:`widgetastic.browser.Browser` or :py:class:`Widget` instance as the first argument. Args: view_class: A subclass of :py:class:`View` """ return WidgetDescriptor(view_class) @property def is_displayed(self): """Overrides the :py:meth:`Widget.is_displayed`. The difference is that if the view does not have the root locator, it assumes it is displayed. Returns: :py:class:`bool` """ try: return super(View, self).is_displayed except (LocatorNotImplemented, AttributeError): return True def move_to(self): """Overrides the :py:meth:`Widget.move_to`. The difference is that if the view does not have the root locator, it returns None. Returns: :py:class:`selenium.webdriver.remote.webelement.WebElement` instance or ``None``. """ try: return super(View, self).move_to() except LocatorNotImplemented: return None def fill(self, values): """Implementation of form filling. This method goes through all widgets defined on this view one by one and calls their ``fill`` methods appropriately. ``None`` values will be ignored. It will log any skipped fill items. It will log a warning if you pass any extra values for filling. It will store the fill value in :py:attr:`last_fill_data`. The data will be "deflattened" to ensure uniformity. Args: values: A dictionary of ``widget_name: value_to_fill``. Returns: :py:class:`bool` if the fill changed any value. """ changed = [] values = deflatten_dict(values) self.last_fill_data = values changed.append(self.before_fill(values)) extra_keys = set(values.keys()) - set(self.widget_names) if extra_keys: self.logger.warning( 'Extra values that have no corresponding fill fields passed: %s', ', '.join(extra_keys)) to_fill = [(getattr(self, n), values[n]) for n in self.widget_names if n in values and values[n] is not None] changed.append(self.fill_strategy.do_fill(to_fill)) a_fill = self.after_fill(any(changed)) return a_fill if isinstance(a_fill, bool) else any(changed) def read(self): """Reads the contents of the view and presents them as a dictionary. Returns: A :py:class:`dict` of ``widget_name: widget_read_value`` where the values are retrieved using the :py:meth:`Widget.read`. """ result = {} for widget_name in self.widget_names: widget = getattr(self, widget_name) try: value = widget.read() except (NotImplementedError, NoSuchElementException, DoNotReadThisWidget): continue result[widget_name] = value return result def before_fill(self, values): """A hook invoked before the loop of filling is invoked. If it returns None, the ``was_changed`` in :py:meth:`fill` does not change. If it returns a boolean, then on ``True`` it modifies the ``was_changed`` to True as well. Args: values: The same values that are passed to :py:meth:`fill` """ pass def after_fill(self, was_change): """A hook invoked after all the widgets were filled. If it returns None, the ``was_changed`` in :py:meth:`fill` does not change. If it returns a boolean, that boolean will be returned as ``was_changed``. Args: was_change: :py:class:`bool` signalizing whether the :py:meth:`fill` changed anything, """ pass