Ejemplo n.º 1
0
def test_unbound_error():
    class MyBasket(Basket):
        orange = Fruit(taste='sour')

    expected = 'fruits of MyBasket is not bound, look in _declared_members[fruits] for the declared copy of this, or bind first'

    basket = MyBasket()
    assert repr(basket.fruits) == expected

    with pytest.raises(NotBoundYetException) as e:
        items(basket.fruits)

    with pytest.raises(NotBoundYetException) as e2:
        str(basket.fruits)

    with pytest.raises(NotBoundYetException) as e3:
        keys(basket.fruits)

    with pytest.raises(NotBoundYetException) as e4:
        values(basket.fruits)

    with pytest.raises(NotBoundYetException) as e5:
        for _ in basket.fruits:
            pass  # pragma: no cover as it is supposed to raise on iter

    assert str(e.value) == str(e2.value) == str(e3.value) == str(
        e4.value) == str(e5.value)
    assert str(e.value) == expected
Ejemplo n.º 2
0
def test_traverse():
    bar = Struct(
        _name='bar',
        _declared_members=dict(
            baz=Struct(_name='baz'),
            buzz=Struct(_name='buzz'),
        ),
    )
    foo = Struct(
        _name='foo',
        _declared_members=dict(bar=bar, ),
    )
    root = StubTraversable(
        _name='root',
        members=Struct(foo=foo),
    )

    expected = {
        '': '',
        'foo': 'foo',
        'bar': 'foo/bar',
        'baz': 'foo/bar/baz',
        'buzz': 'foo/bar/buzz',
    }
    actual = build_long_path_by_path(root)
    assert items(actual) == items(expected)
    assert len(keys(actual)) == len(set(keys(actual)))
Ejemplo n.º 3
0
def collect_members(container, *, name: str, items_dict: Dict = None, items: Dict[str, Any] = None, cls: Type, unknown_types_fall_through=False):
    forbidden_names = FORBIDDEN_NAMES & (set(keys(items_dict or {})) | set(keys(items or {})))
    if forbidden_names:
        raise ForbiddenNamesException(f'The names {", ".join(sorted(forbidden_names))} are reserved by iommi, please pick other names')

    assert name != 'items'
    unbound_items = Struct()
    _unapplied_config = {}

    if items_dict is not None:
        for key, x in items_of(items_dict):
            x._name = key
            unbound_items[key] = x

    if items is not None:
        for key, item in items_of(items):
            if isinstance(item, Traversable):
                # noinspection PyProtectedMember
                assert not item._is_bound
                item._name = key
                unbound_items[key] = item
            elif isinstance(item, dict):
                if key in unbound_items:
                    _unapplied_config[key] = item
                else:
                    item = setdefaults_path(
                        Namespace(),
                        item,
                        call_target__cls=cls,
                        _name=key,
                    )
                    unbound_items[key] = item()
            else:
                assert unknown_types_fall_through or item is None, f'I got {type(item)} when creating a {cls.__name__}.{key}, but I was expecting Traversable or dict'
                unbound_items[key] = item

    for k, v in items_of(Namespace(_unapplied_config)):
        unbound_items[k] = unbound_items[k].reinvoke(v)
        # noinspection PyProtectedMember
        assert unbound_items[k]._name is not None

    to_delete = {
        k
        for k, v in items_of(unbound_items)
        if v is None
    }

    for k in to_delete:
        del unbound_items[k]

    sort_after(unbound_items)

    set_declared_member(container, name, unbound_items)
    setattr(container, name, NotBoundYet(container, name))
Ejemplo n.º 4
0
def test_all_action_shortcuts():
    class MyFancyAction(Action):
        class Meta:
            extra__fancy = True

    class ThingWithActions(Traversable):
        @dispatch
        def __init__(self, actions):
            super(ThingWithActions, self).__init__()
            collect_members(self, name='actions', items=actions, cls=MyFancyAction)

        def on_bind(self):
            bind_members(self, name='actions')

    all_shortcut_names = keys(get_members(
        cls=MyFancyAction,
        member_class=Shortcut,
        is_member=is_shortcut,
    ))

    thing = ThingWithActions(
        actions__action_of_type_icon__icon='flower',
        **{
            f'actions__action_of_type_{t}__call_target__attribute': t
            for t in all_shortcut_names
        },
    ).bind()

    for name, column in items(thing.actions):
        assert column.extra.get('fancy'), name
Ejemplo n.º 5
0
def sorts_right(objects):
    assert {y.expected_position
            for y in values(objects)
            } == set(range(len(objects))), "Borken test"
    sort_after(objects)
    assert [x.expected_position for x in values(objects)
            ] == list(range(len(objects))), keys(objects)
Ejemplo n.º 6
0
def find_target(*, path, root):
    assert path.startswith(DISPATCH_PATH_SEPARATOR)
    p = path[1:]

    long_path = get_long_path_by_path(root).get(p)
    if long_path is None:
        long_path = p
        if long_path not in keys(get_path_by_long_path(root)):

            def format_paths(paths):
                return '\n        '.join(
                    ["''" if not x else x for x in keys(paths)])

            raise InvalidEndpointPathException(
                f"Given path {path} not found.\n"
                f"    Short alternatives:\n        {format_paths(get_long_path_by_path(root))}\n"
                f"    Long alternatives:\n        {format_paths(get_path_by_long_path(root))}"
            )

    node = root
    for part in long_path.split('/'):
        if part == '':
            continue
        next_node = node.iommi_bound_members().get(part)
        assert next_node is not None, f"Failed to traverse long path '{long_path}' (No bound value for '{part}')"
        node = next_node

    return node
Ejemplo n.º 7
0
def evaluate(func_or_value,
             __signature=None,
             __strict=False,
             __match_empty=True,
             **kwargs):
    if callable(func_or_value):
        if __signature is None:
            __signature = signature_from_kwargs(kwargs)

        callee_parameters = get_signature(func_or_value)
        if callee_parameters is not None and matches(
                __signature, callee_parameters, __match_empty):
            return func_or_value(**kwargs)

        if __strict:
            assert (
                    isinstance(func_or_value, Namespace)
                    and 'call_target' not in func_or_value
            ), "Evaluating {} didn't resolve it into a value but strict mode was active, " \
               "the signature doesn't match the given parameters. " \
               "We had these arguments: {}".format(
                get_callable_description(func_or_value),
                ', '.join(keys(kwargs)),
            )
    return func_or_value
Ejemplo n.º 8
0
def test_all_filter_shortcuts():
    class MyFancyFilter(Filter):
        class Meta:
            extra__fancy = True

    class MyFancyQuery(Query):
        class Meta:
            member_class = MyFancyFilter

    all_shortcut_names = keys(
        get_members(
            cls=MyFancyFilter,
            member_class=Shortcut,
            is_member=is_shortcut,
        )
    )

    config = {f'filters__filter_of_type_{t}__call_target__attribute': t for t in all_shortcut_names}

    type_specifics = Namespace(
        filters__filter_of_type_choice__choices=[],
        filters__filter_of_type_multi_choice__choices=[],
        filters__filter_of_type_choice_queryset__choices=TFoo.objects.none(),
        filters__filter_of_type_multi_choice_queryset__choices=TFoo.objects.none(),
        filters__filter_of_type_many_to_many__model_field=TBaz.foo.field,
        filters__filter_of_type_foreign_key__model_field=TBar.foo.field,
    )

    query = MyFancyQuery(**config, **type_specifics).bind(request=req('get'))

    for name, filter in items(query.filters):
        assert filter.extra.get('fancy'), name
Ejemplo n.º 9
0
def test_traverse_on_iommi():
    class MyPage(Page):
        header = Fragment()
        some_form = Form(fields=Namespace(fisk=Field(), ))
        some_other_form = Form(fields=Namespace(
            fjomp=Field(),
            fisk=Field(),
        ))
        a_table = Table(
            model=TFoo,
            columns=Namespace(
                columns=Column(),
                fusk=Column(attr='b', filter__include=True),
            ),
        )

    page = MyPage()

    actual = build_long_path_by_path(page)
    assert len(actual.keys()) == len(set(actual.keys()))
    page = page.bind(request=req('get'))

    assert page.iommi_path == ''
    assert page.parts.header.iommi_path == 'header'
    assert page.parts.some_form.fields.fisk.iommi_path == 'fisk'
    assert page.parts.some_other_form.fields.fisk.iommi_path == 'some_other_form/fisk'
    assert page.parts.a_table.query.form.iommi_path == 'form'
    assert page.parts.a_table.query.form.fields.fusk.iommi_path == 'fusk'
    assert page.parts.a_table.columns.fusk.iommi_path == 'a_table/fusk'
    assert page._name == 'root'
    assert set(keys(page.iommi_evaluate_parameters())) == {
        'traversable', 'page', 'request'
    }
Ejemplo n.º 10
0
def test_choice_queryset():
    foos = [Foo.objects.create(foo=5), Foo.objects.create(foo=7)]

    # make sure we get either 1 or 3 objects later when we choose a random pk
    Bar.objects.create(foo=foos[0])
    Bar.objects.create(foo=foos[1])
    Bar.objects.create(foo=foos[1])
    Bar.objects.create(foo=foos[1])

    class Query2(Query):
        foo = Filter.choice_queryset(
            choices=Foo.objects.all(),
            field__include=True,
            search_fields=['foo'],
        )

    random_valid_obj = Foo.objects.all().order_by('?')[0]

    # test GUI
    form = (
        Query2()
        .bind(
            request=req('get', **{'-': '-', 'foo': 'asdasdasdasd'}),
        )
        .form
    )
    assert not form.is_valid()
    query2 = Query2().bind(request=req('get', **{'-': '-', 'foo': str(random_valid_obj.pk)}))
    form = query2.form
    assert form.is_valid(), form.get_errors()
    assert set(form.fields['foo'].choices) == set(Foo.objects.all())
    q = query2.get_q()
    assert set(Bar.objects.filter(q)) == set(Bar.objects.filter(foo__pk=random_valid_obj.pk))

    # test searching for something that does not exist
    query2 = Query2().bind(
        request=req('get', **{'-': '-', query2.get_advanced_query_param(): 'foo=%s' % str(11)}),
    )
    value_that_does_not_exist = 11
    assert Foo.objects.filter(foo=value_that_does_not_exist).count() == 0
    with pytest.raises(QueryException) as e:
        query2.get_q()
    assert ('Unknown value "%s" for filter "foo"' % value_that_does_not_exist) in str(e)

    # test invalid ops
    valid_ops = ['=']
    for invalid_op in [op for op in keys(Q_OPERATOR_BY_QUERY_OPERATOR) if op not in valid_ops]:
        query2 = Query2().bind(
            request=req(
                'get',
                **{'-': '-', query2.get_advanced_query_param(): 'foo%s%s' % (invalid_op, str(random_valid_obj.foo))},
            ),
        )
        with pytest.raises(QueryException) as e:
            query2.get_q()
        assert ('Invalid operator "%s" for filter "foo"' % invalid_op) in str(e)

    # test a string with the contents "null"
    assert repr(query2.parse_query_string('foo="null"')) == repr(Q(foo=None))
Ejemplo n.º 11
0
def create_members_from_model(*,
                              member_class,
                              model,
                              member_params_by_member_name,
                              include: List[str] = None,
                              exclude: List[str] = None):
    members = Struct()

    check_list(model, include, 'include')
    check_list(model, exclude, 'exclude')

    def create_declared_member(model_field_name):
        definition_or_member = member_params_by_member_name.pop(
            model_field_name, {})
        name = model_field_name.replace('__', '_')
        if isinstance(definition_or_member, dict):
            definition = setdefaults_path(
                Namespace(),
                definition_or_member,
                _name=name,
                # TODO: this should work, but there's a bug in tri.declarative, working around for now
                # call_target__attribute='from_model' if definition_or_member.get('attr', model_field_name) is not None else None,
                call_target__cls=member_class,
            )
            if definition_or_member.get('attr', model_field_name) is not None:
                setdefaults_path(
                    definition,
                    call_target__attribute='from_model',
                )

            member = definition(
                model=model,
                model_field_name=definition_or_member.get(
                    'attr', model_field_name),
            )
        else:
            member = definition_or_member
        if member is None:
            return
        members[name] = member

    model_field_names = include if include is not None else [
        field.name for field in get_fields(model)
    ]

    for model_field_name in model_field_names:
        if exclude is not None and model_field_name in exclude:
            continue
        create_declared_member(model_field_name)

    for model_field_name in list(keys(member_params_by_member_name)):
        create_declared_member(model_field_name)

    return members
Ejemplo n.º 12
0
class StyleSelector(Form):
    class Meta:
        actions__submit__post_handler = select_style_post_handler

    style = Field.choice(
        choices=[
            k for k in keys(iommi.style._styles)
            if k not in ('test', 'base', 'bootstrap_horizontal')
        ],
        initial=lambda form, field, **_: iommi.style.DEFAULT_STYLE,
    )
Ejemplo n.º 13
0
    def __init__(self, parent: Members, _declared_members: Dict[str, Traversable], _unknown_types_fall_through: bool):
        if _unknown_types_fall_through:
            bindable_names = []
            for name, member in items(_declared_members):
                if not hasattr(member, 'bind'):
                    self[name] = copy(member)
                    continue
                bindable_names.append(name)
        else:
            bindable_names = list(keys(_declared_members))

        object.__setattr__(self, '_parent', parent)
        object.__setattr__(self, '_bindable_names', bindable_names)
        object.__setattr__(self, '_declared_members', _declared_members)
        super().__init__()
Ejemplo n.º 14
0
class ShortcutSelectorForm(Form):
    class Meta:
        attrs__method = 'get'

    shortcut = Field.multi_choice(choices=[
        t for t in keys(
            get_members(
                cls=Column, member_class=Shortcut, is_member=is_shortcut))
        if t not in [
            'icon',
            'foreign_key',
            'many_to_many',
            'choice_queryset',
            'multi_choice_queryset',
        ]
    ])
Ejemplo n.º 15
0
Archivo: views.py Proyecto: tltx/iommi
class StyleSelector(Form):
    class Meta:
        @staticmethod
        def actions__submit__post_handler(request, form, **_):
            style = form.fields.style.value
            settings.IOMMI_DEFAULT_STYLE = style
            return HttpResponseRedirect(request.get_full_path())

    style = Field.choice(
        choices=[
            k for k in keys(iommi.style._styles)
            if k not in ('test', 'base') and not k.endswith('_horizontal')
        ],
        initial=lambda form, field, **_: getattr(
            settings, 'IOMMI_DEFAULT_STYLE', iommi.style.DEFAULT_STYLE),
    )
Ejemplo n.º 16
0
def all_field_sorts(request):
    some_choices = ['Foo', 'Bar', 'Baz']
    return Page(parts=dict(
        header=Header('All sorts of fields'),
        form=Form(
            fields={
                f'{t}__call_target__attribute': t
                for t in keys(get_members(
                    cls=Field,
                    member_class=Shortcut,
                    is_member=is_shortcut
                ))
                if t not in [
                    # These only work if we have an instance
                    'foreign_key',
                    'many_to_many']
            },
            fields__radio__choices=some_choices,
            fields__choice__choices=some_choices,
            fields__choice_queryset__choices=TFoo.objects.all(),
            fields__multi_choice__choices=some_choices,
            fields__multi_choice_queryset__choices=TBar.objects.all(),
            fields__info__value="This is some information",
            fields__text__initial='Text',
            fields__textarea__initial='text area\nsecond row',
            fields__integer__initial=3,
            fields__float__initial=3.14,
            fields__password__initial='abc123',
            fields__boolean__initial=True,
            fields__datetime__initial=datetime.now(),
            fields__date__initial=date.today(),
            fields__time__initial=datetime.now().time(),
            fields__decimal__initial=3.14,
            fields__url__initial='http://iommi.rocks',
            fields__email__initial='*****@*****.**',
            fields__phone_number__initial='+1 555 555',

            actions__submit__include=False,
        )
    ))
Ejemplo n.º 17
0
def collect_members(
    container,
    *,
    name: str,
    items_dict: Dict = None,
    items: Dict[str, Any] = None,
    cls: Type,
    unknown_types_fall_through=False,
):
    """
    This function is used to collect and merge data from the constructor
    argument, the declared members, and other config into one data structure.
    `bind_members` is then used at bind time to recursively bind the nested
    parts.

    Example:

    .. code:: python

        class ArtistTable(Table):
            instrument = Column()  # <- declared member

        MyTable(
            columns__name=Column(),  # <- constructor argument
            columns__instrument__after='name',  # <- inserted config for a declared member
        )

    In this example the resulting table will have two columns `instrument` and
    `name`, with `instrument` after name even though it was declared before.
    """
    forbidden_names = FORBIDDEN_NAMES & (set(keys(items_dict or {})) | set(keys(items or {})))
    if forbidden_names:
        raise ForbiddenNamesException(
            f'The names {", ".join(sorted(forbidden_names))} are reserved by iommi, please pick other names'
        )

    assert name != 'items'
    unbound_items = Struct()
    _unapplied_config = {}

    if items_dict is not None:
        for key, x in items_of(items_dict):
            x._name = key
            unbound_items[key] = x

    if items is not None:
        for key, item in items_of(items):
            if isinstance(item, Traversable):
                # noinspection PyProtectedMember
                assert not item._is_bound
                item._name = key
                unbound_items[key] = item
            elif isinstance(item, dict):
                if key in unbound_items:
                    _unapplied_config[key] = item
                else:
                    item = setdefaults_path(
                        Namespace(),
                        item,
                        call_target__cls=cls,
                        _name=key,
                    )
                    unbound_items[key] = item()
            else:
                assert (
                    unknown_types_fall_through or item is None
                ), f'I got {type(item)} when creating a {cls.__name__}.{key}, but I was expecting Traversable or dict'
                unbound_items[key] = item

    for k, v in items_of(Namespace(_unapplied_config)):
        unbound_items[k] = reinvoke(unbound_items[k], v)
        # noinspection PyProtectedMember
        assert unbound_items[k]._name is not None

    to_delete = {k for k, v in items_of(unbound_items) if v is None}

    for k in to_delete:
        del unbound_items[k]

    sort_after(unbound_items)

    set_declared_member(container, name, unbound_items)
    setattr(container, name, NotBoundYet(container, name))
Ejemplo n.º 18
0
def validate_styles(*,
                    additional_classes: List[Type] = None,
                    default_classes=None,
                    styles=None):
    """
    This function validates all registered styles against all standard
    classes. If you have more classes you need to have checked against,
    pass these as the `classes` argument.

    The `default_classes` parameter can be used to say which classes are
    checked for valid data. By default this is all the `Part`-derived
    classes in iommmi. This parameter is primarily used by tests.

    The `styles` parameter can be used to specify which exact styles to
    validate. By default it will validate all registered styles. This
    parameter is primarily used by tests.
    """
    if default_classes is None:
        from iommi import (
            Action,
            Column,
            Field,
            Form,
            Menu,
            MenuItem,
            Query,
            Table,
            Filter,
        )
        from iommi.table import Paginator
        from iommi.menu import (
            MenuBase,
            DebugMenu,
        )
        from iommi.error import Errors
        from iommi.action import Actions
        from iommi.admin import Admin
        from iommi.fragment import Container
        from iommi.fragment import Header
        default_classes = [
            Action,
            Actions,
            Column,
            DebugMenu,
            Errors,
            Field,
            Form,
            Menu,
            MenuBase,
            MenuItem,
            Paginator,
            Query,
            Table,
            Filter,
            Admin,
            Container,
            Header,
        ]
    if additional_classes is None:
        additional_classes = []

    classes = default_classes + additional_classes

    if styles is None:
        styles = _styles

    # We can have multiple classes called Field. In fact that's the recommended way to use iommi!
    classes_by_name = defaultdict(list)
    for cls in classes:
        for cls_name in class_names_for(cls):
            classes_by_name[cls_name].append(cls)

    # This will functionally merge separate trees of class inheritance. So it produces a list of all shortcuts on all classes called something.Field.
    shortcuts_available_by_class_name = defaultdict(set)
    for cls_name, classes in items(classes_by_name):
        for cls in classes:
            shortcuts_available_by_class_name[cls_name].update(
                get_shortcuts_by_name(cls).keys())

    invalid_class_names = []
    non_existent_shortcut_names = []
    for style_name, style in items(styles):
        for cls_name, config in items(style.config):
            # First validate the top level classes
            if cls_name not in classes_by_name:
                invalid_class_names.append((style_name, cls_name))
                continue

            # Then validate the shortcuts
            for shortcut_name in keys(config.get('shortcuts', {})):
                if shortcut_name not in shortcuts_available_by_class_name[
                        cls_name]:
                    non_existent_shortcut_names.append(
                        (style_name, cls_name, shortcut_name))

    if invalid_class_names or non_existent_shortcut_names:
        invalid_class_names_str = '\n'.join(
            f'    Style: {style_name} - class: {cls_name}'
            for style_name, cls_name in invalid_class_names)
        if invalid_class_names_str:
            invalid_class_names_str = 'Invalid class names:\n' + invalid_class_names_str
        invalid_shortcut_names_str = '\n'.join(
            f'    Style: {style_name} - class: {cls_name} - shortcut: {shortcut_name}'
            for style_name, cls_name, shortcut_name in
            non_existent_shortcut_names)
        if invalid_shortcut_names_str:
            invalid_shortcut_names_str = 'Invalid shortcut names:\n' + invalid_shortcut_names_str
        raise InvalidStyleConfigurationException('\n\n'.join(
            [invalid_class_names_str, invalid_shortcut_names_str]))
Ejemplo n.º 19
0
def signature_from_kwargs(kwargs):
    return ','.join(sorted(keys(kwargs)))
Ejemplo n.º 20
0
 def format_paths(paths):
     return '\n        '.join(
         ["''" if not x else x for x in keys(paths)])
Ejemplo n.º 21
0
def create_members_from_model(*,
                              member_class,
                              model,
                              member_params_by_member_name,
                              include: List[str] = None,
                              exclude: List[str] = None):
    def should_include(name):
        if exclude is not None and name in exclude:
            return False
        if include is not None:
            return name in include
        return True

    members = Struct()

    # Validate include/exclude parameters
    field_names = {x.name for x in get_fields(model)}

    def check_list(l, name):
        if l:
            not_existing = {x for x in l if x not in field_names}
            existing = "\n    ".join(sorted(field_names))
            assert not not_existing, f'You can only {name} fields that exist on the model: {", ".join(sorted(not_existing))} specified but does not exist\nExisting fields:\n    {existing}'

    check_list(include, 'include')
    check_list(exclude, 'exclude')

    def create_declared_member(model_field_name):
        definition_or_member = member_params_by_member_name.pop(
            model_field_name, {})
        if isinstance(definition_or_member, dict):
            definition = setdefaults_path(
                Namespace(),
                definition_or_member,
                # TODO: this should work, but there's a bug in tri.declarative, working around for now
                # call_target__attribute='from_model' if definition_or_member.get('attr', model_field_name) is not None else None,
                call_target__cls=member_class,
            )
            if definition_or_member.get('attr', model_field_name) is not None:
                setdefaults_path(
                    definition,
                    call_target__attribute='from_model',
                )

            member = definition(
                model=model,
                model_field_name=definition_or_member.get(
                    'attr', model_field_name),
            )
        else:
            member = definition_or_member
        if member is None:
            return
        members[model_field_name] = member

    for field in get_fields(model):
        if should_include(field.name):
            create_declared_member(field.name)

    for model_field_name in list(keys(member_params_by_member_name)):
        create_declared_member(model_field_name)

    # We respect the order given by `include`
    if include is not None:

        def index(x):
            try:
                return include.index(x[0])
            except ValueError:
                return len(members) + 1  # last!

        members = {k: v for k, v in sorted(items(members), key=index)}

    return members
Ejemplo n.º 22
0
def test_traverse_on_iommi():
    class MyPage(Page):
        header = Fragment()
        some_form = Form(fields=Namespace(fisk=Field(), ))
        some_other_form = Form(fields=Namespace(
            fjomp=Field(),
            fisk=Field(),
        ))
        a_table = Table(
            model=TFoo,
            columns=Namespace(
                columns=Column(),
                fusk=Column(attr='b', filter__include=True),
            ),
        )

    page = MyPage()

    actual = build_long_path_by_path(page)
    assert actual == {
        '': 'parts/header',
        'a_table': 'parts/a_table',
        'a_table/columns': 'parts/a_table/columns/columns',
        'a_table/fusk': 'parts/a_table/columns/fusk',
        'a_table/select': 'parts/a_table/columns/select',
        'advanced': 'parts/a_table/query/advanced',
        'columns': 'parts/a_table/query/form/fields/columns',
        'columns/config':
        'parts/a_table/query/form/fields/columns/endpoints/config',
        'columns/validate':
        'parts/a_table/query/form/fields/columns/endpoints/validate',
        'config': 'parts/some_form/fields/fisk/endpoints/config',
        'csv': 'parts/a_table/endpoints/csv',
        'errors': 'parts/a_table/query/endpoints/errors',
        'fisk': 'parts/some_form/fields/fisk',
        'fisk/config': 'parts/some_other_form/fields/fisk/endpoints/config',
        'fisk/validate':
        'parts/some_other_form/fields/fisk/endpoints/validate',
        'fjomp': 'parts/some_other_form/fields/fjomp',
        'fjomp/config': 'parts/some_other_form/fields/fjomp/endpoints/config',
        'fjomp/validate':
        'parts/some_other_form/fields/fjomp/endpoints/validate',
        'form': 'parts/a_table/query/form',
        'freetext_search': 'parts/a_table/query/form/fields/freetext_search',
        'freetext_search/config':
        'parts/a_table/query/form/fields/freetext_search/endpoints/config',
        'freetext_search/validate':
        'parts/a_table/query/form/fields/freetext_search/endpoints/validate',
        'fusk': 'parts/a_table/query/form/fields/fusk',
        'fusk/config': 'parts/a_table/query/form/fields/fusk/endpoints/config',
        'fusk/validate':
        'parts/a_table/query/form/fields/fusk/endpoints/validate',
        'page': 'parts/a_table/parts/page',
        'query': 'parts/a_table/query',
        'query/columns': 'parts/a_table/query/filters/columns',
        'query/fusk': 'parts/a_table/query/filters/fusk',
        'query/select': 'parts/a_table/query/filters/select',
        'query_form_toggle_script':
        'parts/a_table/assets/query_form_toggle_script',
        'select': 'parts/a_table/query/form/fields/select',
        'select/config':
        'parts/a_table/query/form/fields/select/endpoints/config',
        'select/validate':
        'parts/a_table/query/form/fields/select/endpoints/validate',
        'some_form': 'parts/some_form',
        'some_other_form': 'parts/some_other_form',
        'some_other_form/fisk': 'parts/some_other_form/fields/fisk',
        'submit': 'parts/a_table/query/form/actions/submit',
        'table_js_select_all': 'parts/a_table/assets/table_js_select_all',
        'tbody': 'parts/a_table/endpoints/tbody',
        'toggle': 'parts/a_table/query/advanced/toggle',
        'validate': 'parts/some_form/fields/fisk/endpoints/validate',
    }
    assert len(actual.values()) == len(set(actual.values()))
    page = page.bind(request=req('get'))

    assert page.iommi_path == ''
    assert page.parts.header.iommi_path == 'header'
    assert page.parts.some_form.fields.fisk.iommi_path == 'fisk'
    assert page.parts.some_other_form.fields.fisk.iommi_path == 'some_other_form/fisk'
    assert page.parts.a_table.query.form.iommi_path == 'form'
    assert page.parts.a_table.query.form.fields.fusk.iommi_path == 'fusk'
    assert page.parts.a_table.columns.fusk.iommi_path == 'a_table/fusk'
    assert page._name == 'root'
    assert set(keys(page.iommi_evaluate_parameters())) == {
        'traversable', 'page', 'request'
    }