def correct_function_called(self): @arg_checks(y=IsInstance(int), title=IsInstance(str)) def do_something(x, y, title='a title', style=None): pass self.expected_exception = NoException self.args = (1, 2) self.call_args = (1, 2) self.kwargs = {} self.callable = do_something
class ADeprecatedClass: @deprecated('this instance method is deprecated', '1.3') @arg_checks(y=IsInstance(int), title=IsInstance(str)) def instance_method(self, x, y, title='a title', style=None): pass @deprecated('this class method is deprecated', '2.3') @arg_checks(y=IsInstance(int), title=IsInstance(str)) @classmethod def class_method(cls, x, y, title='a title', style=None): pass
class ChoicesLayout(reahl.web.fw.Layout): def __init__(self, inline=False): super(ChoicesLayout, self).__init__() self.inline = inline @arg_checks(html_input=IsInstance((PrimitiveCheckboxInput, SingleChoice))) def add_choice(self, html_input): input_type_custom_control = HTMLAttributeValueOption(html_input.choice_type, True, prefix='custom', constrain_value_to=['radio', 'checkbox']) label_widget = Label(self.view, for_input=html_input) label_widget.append_class('custom-control-label') html_input.append_class('custom-control-input') outer_div = Div(self.view) outer_div.append_class('custom-control') outer_div.append_class(input_type_custom_control.as_html_snippet()) if self.inline: outer_div.append_class('custom-control-inline') if html_input.disabled: outer_div.append_class('disabled') outer_div.add_child(html_input) outer_div.add_child(label_widget) self.widget.add_child(outer_div) return outer_div
class ColumnOptions: """Various options to change how a column should be displayed. :param name: The name of the column. :keyword size: The :class:`ResponsiveSize` of the column. :keyword offsets: A :class:`ResponsiveSize` representing extra space before the column. :keyword vertical_align: An :class:`Alignment` stating how this column should be aligned vertically in its container. .. versionadded:: 4.0 """ @arg_checks(size=IsInstance(ResponsiveSize, allow_none=True), offsets=IsInstance(ResponsiveSize, allow_none=True), vertical_align=IsInstance(Alignment, allow_none=True)) def __init__(self, name, size=None, offsets=None, vertical_align=None): self.name = name self.size = size or ResponsiveSize(xs=True) self.offsets = offsets or ResponsiveSize() self.vertical_align = vertical_align or Alignment()
def test_stubbable_is_instance(): """Classes can be marked with a flag to let them pass the IsInstance or IsSubclass checks even though they do not inherit from the specified class.""" class A(object): pass class B(object): is_A = True with expected(NoException): assert IsInstance(A).is_valid(B()) with expected(NoException): assert IsSubclass(A).is_valid(B)
class PageLayout(Layout): """A PageLayout creates a basic skeleton inside an :class:`reahl.web.ui.HTML5Page`, and optionally applies specified :class:`~reahl.web.fw.Layout`\s to parts of this skeleton. The skeleton consists of a :class:`~reahl.web.ui.Div` called the `document` of the page, which contains three sub-sections inside of it: - the `.header` -- the page header area where menus and banners go; - the `.contents` of the page -- the main area to which the main content will be added; and - the `.footer` -- the page footer where links and legal notices go. :keyword document_layout: A :class:`~reahl.web.fw.Layout` that will be applied to `.document`. :keyword contents_layout: A :class:`~reahl.web.fw.Layout` that will be applied to `.contents`. :keyword header_layout: A :class:`~reahl.web.fw.Layout` that will be applied to `.header`. :keyword footer_layout: A :class:`~reahl.web.fw.Layout` that will be applied to `.footer`. .. admonition:: Styling Adds a <div id="doc"> to the <body> of the page, which contains: - a <header id="hd"> - a <div id="contents"> - a <footer id="ft"> .. versionadded:: 3.2 """ @arg_checks(document_layout=IsInstance(Layout, allow_none=True), contents_layout=IsInstance(Layout, allow_none=True), header_layout=IsInstance(Layout, allow_none=True), footer_layout=IsInstance(Layout, allow_none=True)) def __init__(self, document_layout=None, contents_layout=None, header_layout=None, footer_layout=None): super(PageLayout, self).__init__() self.header = None #: The :class:`reahl.web.ui.Header` of the page. self.contents = None #: The :class:`reahl.web.ui.Div` containing the contents. self.footer = None #: The :class:`reahl.web.ui.Footer` of the page. self.document = None #: The :class:`reahl.web.ui.Div` containing the entire page. self.header_layout = header_layout #: A :class:`reahl.web.fw.Layout` to be used for the header of the page. self.contents_layout = contents_layout #: A :class:`reahl.web.fw.Layout` to be used for the contents div of the page. self.footer_layout = footer_layout #: A :class:`reahl.web.fw.Layout` to be used for the footer of the page. self.document_layout = document_layout #: A :class:`reahl.web.fw.Layout` to be used for the document of the page. @arg_checks(widget=IsInstance(HTML5Page)) def apply_to_widget(self, widget): super(PageLayout, self).apply_to_widget(widget) def customise_widget(self): self.document = self.widget.body.add_child(Div(self.view)) self.document.set_id('doc') if self.document_layout: self.document.use_layout(self.document_layout) self.header = self.document.add_child(Header(self.view)) self.header.add_child(Slot(self.view, 'header')) self.header.set_id('hd') if self.header_layout: self.header.use_layout(self.header_layout) self.contents = self.document.add_child(Div(self.view)) if self.contents_layout: self.contents.use_layout(self.contents_layout) self.contents.set_id('bd') self.contents.set_attribute('role', 'main') self.footer = self.document.add_child(Footer(self.view)) self.footer.add_child(Slot(self.view, 'footer')) self.footer.set_id('ft') if self.footer_layout: self.footer.use_layout(self.footer_layout)
class ColumnLayout(Layout): """A Layout that divides an element into a number of columns. Each positional argument passed to the constructor defines a column. Columns are added to the element using this Layout in the order they are passed to the constructor. Columns can also be added to the Widget later, by calling :meth:`ColumnLayout.add_column`. Each such column-defining argument to the constructor is a tuple of which the first element is the column name, and the second an instance of :class:`ColumnOptions`. Besides the size of the column, other adjustments can be made via :class:`ColumnOptions`. You can also pass the column name only (no tuple) in which case a default :class:`ColumnOptions` will be used. If an element is divided into a number of columns whose current combined width is wider than 12/12ths, the overrun flows to make an additional row. It is customary, for example to specify smaller sizes (ito 12ths) for bigger devices where you want the columns to fit in next to each other, but use BIGGER sizes (such as 12/12ths) for the columns for smaller sized devices. This has the effect that what was displayed as columns next to each other on the bigger device is displayed as "stacked" cells on a smaller device. By default, the smallest device classes are sized 12/12ths. By default, if you specify smaller column sizes for larger devices, but omit specifying a size for extra small (xs) devices, columns for xs devices are full width and thus stacked. .. versionchanged:: 4.0 Each item in column_definitions can be just a string (the column name) or a tuple mapping a name to a :class:`ColumnOptions`. """ def __init__(self, *column_definitions): super().__init__() if not all([ isinstance(column_definition, (str, ColumnOptions)) for column_definition in column_definitions ]): raise ProgrammerError( 'All column definitions are expected be either a ColumnOptions object of a column name, got %s' % str(column_definitions)) self.added_column_definitions = [] self.add_slots = False self.add_gutters = True self.alignment = Alignment() self.content_justification = ContentJustification() self.columns = OrderedDict( ) #: A dictionary containing the added columns, keyed by column name. self.column_definitions = OrderedDict() for column_definition in column_definitions: if isinstance(column_definition, str): name, options = column_definition, ColumnOptions( column_definition) else: name, options = column_definition.name, column_definition self.column_definitions[name] = options def with_slots(self): """Returns a copy of this ColumnLayout which will additionally add a Slot inside each added column, named for that column. """ copy_with_slots = copy.deepcopy(self) copy_with_slots.add_slots = True return copy_with_slots def without_gutters(self): """Returns a copy of this ColumnLayout which will not display whitespace between columns. .. versionadded:: 4.0 """ copy_without_gutters = copy.deepcopy(self) copy_without_gutters.add_gutters = False return copy_without_gutters @arg_checks(content_justification=IsInstance(ContentJustification)) def with_justified_content(self, content_justification): """Returns a copy of this ColumnLayout with justification options set on it. .. versionadded:: 4.0 """ copy_with_content_justification = copy.deepcopy(self) copy_with_content_justification.content_justification = content_justification return copy_with_content_justification @arg_checks(vertical_alignment=IsInstance(Alignment)) def with_vertical_alignment(self, vertical_alignment): """Returns a copy of this ColumnLayout with the column alignment options set on it. .. versionadded:: 4.0 """ copy_with_alignment = copy.deepcopy(self) copy_with_alignment.alignment = vertical_alignment return copy_with_alignment def customise_widget(self): for name, options in self.column_definitions.items(): self.add_column(options.name, size=options.size, offsets=options.offsets, vertical_align=options.vertical_align) self.widget.append_class('row') if not self.add_gutters: self.widget.append_class('no-gutters') self.alignment.add_css_classes(self.widget) self.content_justification.add_css_classes(self.widget) def add_clearfix(self, column_options): clearfix = self.widget.add_child(Div(self.view)) clearfix.append_class('clearfix') wrapping_classes = [ device_class for device_class in DeviceClass.all_classes() if ResponsiveSize.wraps_for( device_class, self.added_column_definitions + [column_options]) ] if wrapping_classes: device_class = wrapping_classes[0] if device_class.one_smaller: clearfix.append_class( device_class.one_smaller.as_combined_css_class(['hidden'], [])) return clearfix def add_column(self, name, size=None, offsets=None, vertical_align=None, column_widget=None): """Called to add a column with given options. :param name: (See :class:`ColumnOptions`) :keyword size: (See :class:`ColumnOptions`) :keyword offsets: (See :class:`ColumnOptions`) :keyword vertical_align: (See :class:`ColumnOptions`) :keyword column_widget: If given, this Widget is added as the column instead of a Div (the default). .. versionchanged:: 4.0 Changed to create a named column with all possible options. """ column_options = ColumnOptions(name, size=size, offsets=offsets, vertical_align=vertical_align) if ResponsiveSize.wraps_for_some_device_class( self.added_column_definitions + [column_options]): self.add_clearfix(column_options) column = self.widget.add_child(column_widget or Div(self.view)) column_options.size.add_css_classes(column, prefix='col') column_options.offsets.add_css_classes(column, prefix='offset') column_options.vertical_align.add_css_classes(column) self.added_column_definitions.append(column_options) self.columns[column_options.name] = column column.append_class('column-%s' % column_options.name) if self.add_slots: column.add_child(Slot(self.view, column_options.name)) return column
class ModelObject: @arg_checks(y=IsInstance(int), title=IsInstance(str)) @classmethod def do_something(cls, x, y, title='a title', style=None): pass
class ADeprecatedClass: @arg_checks(y=IsInstance(int), title=IsInstance(str)) def __init__(self, x, y, title='a title', style=None): pass
class NavbarLayout(Layout): """Used to populate a Navbar. :keyword fixed_to: May be one of 'top','bottom' or 'stickytop'. The Navbar will stick to the top or bottom of the viewport. :keyword center_contents: If True, all the contents of the Navbar is centered within the Navbar itself. :keyword colour_theme: Use 'light' for use with light background colors, or 'dark' with dark background colors. :keyword bg_scheme: Whether the Navbar should use 'primary' colors, a 'dark' (light on dark) scheme or a 'light' background. """ def __init__(self, fixed_to=None, center_contents=False, colour_theme=None, bg_scheme=None): super().__init__() self.fixed = NavbarFixed(fixed_to) self.center_contents = center_contents self.colour_theme = ColourTheme(colour_theme) self.bg_scheme = BackgroundScheme(bg_scheme) self.brand = None self.toggle = None self.contents_container = None @property def nav(self): return self.widget.html_representation def customise_widget(self): super().customise_widget() if self.center_contents: centering_div = self.nav.add_child(Div(self.view).use_layout(Container())) self.contents_container = centering_div else: self.contents_container = self.nav self.main_container = self.contents_container if self.fixed.is_set: self.widget.append_class(self.fixed.as_html_snippet()) for option in [self.colour_theme, self.bg_scheme]: if option.is_set: self.nav.append_class(option.as_html_snippet()) def set_brand_text(self, brand_text): """Sets the brand to be a link to the home page that contains the given text. :param brand_text: Text to use for branding. """ brand_a = A(self.view, Url('/'), description=brand_text) self.set_brand(brand_a) def insert_brand_widget(self, brand_html_element): index = 1 if self.toggle else 0 self.main_container.insert_child(index, brand_html_element) @arg_checks(brand_html_element=IsInstance(HTMLWidget)) def set_brand(self, brand_htmlwidget): """Sets `brand_widget` to be used as branding. :param brand_htmlwidget: An :class:`~reahl.web.ui.HTMLWidget` to be used as branding. """ if self.brand: raise ProgrammerError('Brand has already been set to: %s' % self.brand) self.insert_brand_widget(brand_htmlwidget) brand_htmlwidget.append_class('navbar-brand') self.brand = brand_htmlwidget return self.brand @arg_checks(widget=IsInstance((reahl.web.bootstrap.navs.Nav, Form, TextNode))) def add(self, widget): """Adds the given Form or Nav `widget` to the Navbar. :param widget: A :class:`~reahl.web.bootstrap.navs.Nav`, :class:`~reahl.web.bootstrap.ui.Form` or :class:`~reahl.web.bootstrap.ui.TextNode` to add. """ if isinstance(widget, reahl.web.bootstrap.navs.Nav): widget.append_class('navbar-nav') if isinstance(widget, Form): widget.append_class('form-inline') if isinstance(widget, TextNode): span = Span(self.view) span.add_child(widget) span.append_class('navbar-text') widget = span return self.contents_container.add_child(widget) def add_toggle(self, target_html_element, text=None, left_aligned=False): """Adds a link that toggles the display of the given `target_html_element`. :param target_html_element: A :class:`~reahl.web.ui.HTMLElement` :param text: Text to be used on the toggle link. If None, the boostrap navbar-toggler-icon is used :keyword left_aligned: If True, ensure that the toggle is to the far left. """ if not target_html_element.css_id_is_set: raise ProgrammerError('%s has no css_id set. A toggle is required to have a css_id' % target_html_element) target_html_element.append_class('collapse') toggle = CollapseToggle(self.view, target_html_element, text=text) index = 1 if (self.brand and not left_aligned) else 0 self.main_container.insert_child(index, toggle) self.toggle = toggle return toggle
class ModelObject(object): @arg_checks(y=IsInstance(int), title=IsInstance(six.string_types)) @classmethod def do_something(cls, x, y, title='a title', style=None): pass
class ADeprecatedClass(object): @arg_checks(y=IsInstance(int), title=IsInstance(six.string_types)) def __init__(self, x, y, title='a title', style=None): pass
class NavbarLayout(Layout): """Used to populate a Navbar. :keyword fixed_to: If one of 'top' or 'bottom', the Navbar will stick to the top or bottom of the viewport. :keyword full: If True, the Navbar fills the available width. :keyword center_contents: If True, all the contents of the Navbar is centered within the Navbar itself. :keyword colour_theme: Whether the Navbar has a 'dark' or 'light' background. :keyword bg_scheme: Whether the Navbar should use 'primary' colors, an 'inverse' (light on dark) scheme or a 'faded' background. """ def __init__(self, fixed_to=None, full=False, center_contents=False, colour_theme=None, bg_scheme=None): super(NavbarLayout, self).__init__() if fixed_to and full: raise ProgrammerError( 'Both fixed_to and full are given. Give fixed_to or full, but not both' ) self.fixed = NavbarFixed(fixed_to) self.full = HTMLAttributeValueOption('navbar-full', full) self.center_contents = center_contents self.colour_theme = ColourTheme(colour_theme) self.bg_scheme = BackgroundScheme(bg_scheme) self.brand = None self.contents_container = None def customise_widget(self): super(NavbarLayout, self).customise_widget() nav = self.widget.html_representation if self.center_contents: centering_div = nav.add_child( Div(self.view).use_layout(Container())) self.contents_container = centering_div else: self.contents_container = nav for option in [self.fixed, self.full]: if option.is_set: self.widget.append_class(option.as_html_snippet()) for option in [self.colour_theme, self.bg_scheme]: if option.is_set: nav.append_class(option.as_html_snippet()) def set_brand_text(self, brand_text): """Sets the brand to be a link to the home page that contains the given text. :param brand_text: Text to use for branding. """ brand_a = A(self.view, Url('/#'), description=brand_text) self.set_brand(brand_a) @arg_checks(brand_html_element=IsInstance(HTMLElement)) def set_brand(self, brand_html_element): """Sets `brand_html_element` to be used as branding. :param brand_html_element: An :class:`~reahl.web.ui.HTMLElement` to be used as branding. """ if self.brand: raise ProgrammerError('Brand has already been set to: %s' % self.brand) self.contents_container.insert_child(0, brand_html_element) brand_html_element.append_class('navbar-brand') self.brand = brand_html_element @arg_checks(widget=IsInstance((reahl.web.bootstrap.navs.Nav, Form))) def add(self, widget, left=None, right=None): """Adds the given Form or Nav `widget` to the Navbar. :param widget: A :class:`~reahl.web.bootstrap.navs.Nav` or :class:`~reahl.web.bootstrap.ui.Form` to add. :keyword left: If True, `widget` is aligned to the left of the Navbar. :keyword right: If True, `widget` is aligned to the right of the Navbar. """ if isinstance(widget, reahl.web.bootstrap.navs.Nav): widget.append_class('navbar-nav') if left or right: child = Div(self.view).use_layout( ResponsiveFloat(left=left, right=right)) child.add_child(widget) else: child = widget self.contents_container.add_child(child) return widget def add_toggle(self, target_html_element, text=None): """Adds a link that toggles the display of the given `target_html_element`. :param target_html_element: A :class:`~reahl.web.ui.HTMLElement` :keyword text: Text to be used on the toggle link. """ if not target_html_element.css_id_is_set: raise ProgrammerError( '%s has no css_id set. A toggle is required to have a css_id' % target_html_element) target_html_element.append_class('collapse') toggle = CollapseToggle(self.view, target_html_element, text=text) self.contents_container.add_child(toggle) return toggle
class ResponsiveSize(reahl.web.layout.ResponsiveSize): """A size used for layouts that can adapt depending on how big the user device is. Sizes kwargs for each device class are given as integers that denote a number of 12ths of the size of the container of the element being sized. Eg: 6 would mean 6 12ths, or 1/2 the size of the container. If you specify a size for a device class, that size will be used for all devices of that class or bigger. It is not necessary to specify a size for every device class. By default, if a device class is omitted, it is assumed to be sized as per the nearest specified smaller device class. If there is no smaller device class, a value of 12/12ths is assumed. :keyword xs: Size to use if the device is extra small. :keyword sm: Size to use if the device is small. :keyword md: Size to use if the device is medium. :keyword lg: Size to use if the device is large. :keyword xl: Size to use if the device is extra large. """ @arg_checks(xs=IsInstance(int, allow_none=True), sm=IsInstance(int, allow_none=True), md=IsInstance(int, allow_none=True), lg=IsInstance(int, allow_none=True), xl=IsInstance(int, allow_none=True)) def __init__(self, xs=None, sm=None, md=None, lg=None, xl=None): super(ResponsiveSize, self).__init__(xs=xs, sm=sm, md=md, lg=lg, xl=xl) self.offsets = {} @arg_checks(xs=IsInstance(int, allow_none=True), sm=IsInstance(int, allow_none=True), md=IsInstance(int, allow_none=True), lg=IsInstance(int, allow_none=True), xl=IsInstance(int, allow_none=True)) def offset(self, xs=None, sm=None, md=None, lg=None, xl=None): self.offsets = ResponsiveSize(xs=xs, sm=sm, md=md, lg=lg, xl=xl) return self def calculated_size_for(self, device_class): classes_that_impact = [device_class] + device_class.all_smaller for possible_class in reversed(classes_that_impact): try: return self[possible_class.class_label] except KeyError: pass return 0 def total_width_for(self, device_class): total = self.calculated_size_for(device_class) if self.offsets: total += self.offsets.calculated_size_for(device_class) return total @classmethod def wraps_for_some_device_class(cls, sizes): return any([ cls.wraps_for(device_class, sizes) for device_class in DeviceClass.all_classes() ]) @classmethod def wraps_for(cls, device_class, sizes): return (cls.sum_sizes_for(device_class, sizes)) > 12 @classmethod def sum_sizes_for(cls, device_class, sizes): total = 0 for size in sizes: total += size.total_width_for(device_class) return total