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
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)))
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))
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
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)
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
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
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
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' }
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))
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
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, )
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__()
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', ] ])
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), )
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, ) ))
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))
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]))
def signature_from_kwargs(kwargs): return ','.join(sorted(keys(kwargs)))
def format_paths(paths): return '\n '.join( ["''" if not x else x for x in keys(paths)])
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
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' }