Example #1
0
    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
Example #2
0
    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
Example #3
0
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
Example #4
0
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()
Example #5
0
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)
Example #6
0
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)
Example #7
0
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
Example #8
0
 class ModelObject:
     @arg_checks(y=IsInstance(int), title=IsInstance(str))
     @classmethod
     def do_something(cls, x, y, title='a title', style=None):
         pass
Example #9
0
 class ADeprecatedClass:
     @arg_checks(y=IsInstance(int), title=IsInstance(str))
     def __init__(self, x, y, title='a title', style=None):
         pass
Example #10
0
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
Example #11
0
 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
Example #12
0
 class ADeprecatedClass(object):
     @arg_checks(y=IsInstance(int), title=IsInstance(six.string_types))
     def __init__(self, x, y, title='a title', style=None):
         pass
Example #13
0
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
Example #14
0
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