diff --git a/README.rst b/README.rst index 663d0d1b..acc0bf7f 100644 --- a/README.rst +++ b/README.rst @@ -159,6 +159,31 @@ widgets or other browser operations operate within the widget's root element, el of passing the parent element. +Simplified nested form fill +--------------------------- + +When you want to separate widgets into logical groups but you don't want to have a visual clutter in +the code, you can use dots in fill keys to signify the dictionary boundaries: + +.. code-block:: python + + # This: + view.fill({ + 'x': 1, + 'foo.bar': 2, + 'foo.baz': 3, + }) + + # Is equivalent to this: + view.fill({ + 'x': 1, + 'foo': { + 'bar': 2, + 'baz': 3, + } + }) + + .. `Version picking`: Version picking diff --git a/src/widgetastic/utils.py b/src/widgetastic/utils.py index 988fe01c..f85e5900 100644 --- a/src/widgetastic/utils.py +++ b/src/widgetastic/utils.py @@ -477,3 +477,48 @@ def nested_getattr(o, steps): for step in steps: result = getattr(result, step) return result + + +def deflatten_dict(d): + """Expands nested dictionary from dot-separated string keys. + + Useful when one needs filling a nested view, this can reduce the visual nesting + + Turns this: + + .. code-block:: python + + {'a.b': 1} + + Into this: + + .. code-block:: python + + {'a': {'b': 1}} + + The conversion recursively follows dictionaries as values. + + Args: + d: Dictionary + + Returns: + A dictionary. + """ + current_dict = {} + for key, value in six.iteritems(d): + if not isinstance(key, six.string_types): + current_dict[key] = value + continue + local_dict = current_dict + if isinstance(key, tuple): + attrs = list(key) + else: + attrs = [x.strip() for x in key.split('.')] + dict_lookup = attrs[:-1] + attr_set = attrs[-1] + for attr_name in dict_lookup: + if attr_name not in local_dict: + local_dict[attr_name] = {} + local_dict = local_dict[attr_name] + local_dict[attr_set] = deflatten_dict(value) if isinstance(value, dict) else value + return current_dict diff --git a/src/widgetastic/widget.py b/src/widgetastic/widget.py index 44ff76b4..cbe9cf49 100644 --- a/src/widgetastic/widget.py +++ b/src/widgetastic/widget.py @@ -24,7 +24,7 @@ create_item_logger) from .utils import ( Widgetable, Fillable, ParametrizedLocator, ConstructorResolvable, attributize_string, - normalize_space, nested_getattr) + normalize_space, nested_getattr, deflatten_dict) from .xpath import quote @@ -681,6 +681,7 @@ def fill(self, values): Returns: :py:class:`bool` if the fill changed any value. """ + values = deflatten_dict(values) was_change = False self.before_fill(values) extra_keys = set(values.keys()) - set(self.widget_names) diff --git a/testing/test_basic_widgets.py b/testing/test_basic_widgets.py index bdde841d..13da6831 100644 --- a/testing/test_basic_widgets.py +++ b/testing/test_basic_widgets.py @@ -88,6 +88,39 @@ class Nested2(View): assert form.Nested1.Nested2.input2.locatable_parent is None +def test_nested_views_read_fill_flat(browser): + class TestForm(View): + h3 = Text('.//h3') + + class Nested1(View): + input1 = TextInput(name='input1') + + class Nested2(View): + input2 = Checkbox(id='input2') + + form = TestForm(browser) + assert isinstance(form, TestForm) + data = form.read() + + assert data['h3'] == 'test test' + assert data['Nested1']['input1'] == '' + assert not data['Nested1']['Nested2']['input2'] + + assert form.fill({ + 'Nested1.input1': 'foobar', + 'Nested1.Nested2.input2': True, + }) + + assert form.Nested1.input1.read() == 'foobar' + assert form.Nested1.Nested2.input2.read() + + assert form.Nested1.Nested2.input2.hierarchy == [ + form, form.Nested1, form.Nested1.Nested2, form.Nested1.Nested2.input2] + assert form.hierarchy == [form] + + assert form.Nested1.Nested2.input2.locatable_parent is None + + def test_table(browser): class TestForm(View): table = Table('#with-thead')