gwsumm/tabs/core.py
# coding=utf-8
# Copyright (C) Duncan Macleod (2013)
#
# This file is part of GWSumm.
#
# GWSumm is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# GWSumm is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GWSumm. If not, see <http://www.gnu.org/licenses/>.
"""This module defines the core `Tab` object.
The basic Tab allows for simple embedding of arbitrary text inside a
standardised HTML interface. Most real-world applications will use a
sub-class of `Tab` to create more complex HTML output.
The `Tab` class comes in three flavours:
- 'static', no specific GPS time reference
- 'interval', displaying data in a given GPS [start, stop) interval
- 'event', displaying data around a specific central GPS time
The flavour is dynamically set when each instance is created based on the
`mode` keyword, or the presence of `span`, `start and `end`, or `gpstime`
keyword arguments.
"""
import os
import re
from collections import OrderedDict
from configparser import NoOptionError
from MarkupPy import markup
from gwpy.time import (from_gps, to_gps)
from gwpy.segments import Segment
from gwdetchar.io import html as gwhtml
from .. import __version__
from .. import html
from ..mode import (Mode, get_mode, get_base)
from ..utils import (re_quote, re_cchar)
from .registry import (get_tab, register_tab)
__author__ = 'Duncan Macleod <duncan.macleod@ligo.org>'
__all__ = ['BaseTab', 'Tab', 'TabList']
def get_version():
return __version__
# -- BaseTab ------------------------------------------------------------------
# this object defines the basic object from which all three flavours inherit
class BaseTab(object):
"""The core `Tab` object, defining basic functionality
"""
def __init__(self, name, index=None, shortname=None, parent=None,
children=list(), group=None, notes=None, overlay=None,
path=os.curdir, mode=None, hidden=False):
# mode
self.mode = mode
# names
self.name = name
self.shortname = shortname
# structure
self._children = []
self.parent = parent
self.children = children
self.group = group
self.notes = notes
self.overlay = overlay
# HTML format
self.path = path
self.index = index
self.page = None
self.hidden = hidden
# -- properties -----------------------------
@property
def name(self):
"""Full name for this `Tab`
:type: `str`
"""
return self._name
@name.setter
def name(self, n):
self._name = n
@property
def shortname(self):
"""Short name for this tab
This will be displayed in the navigation bar.
:type: `str`
"""
return self._shortname or self.name
@shortname.setter
def shortname(self, name):
self._shortname = name
@property
def parent(self):
"""Short name of the parent page for this `Tab`
A given tab can either be a parent for a set of child tabs, or can
have a parent, it cannot be both. In this system, the `parent`
attribute defines the heading under which this tab will be linked
in the HTML navigation bar.
:type: `str`
"""
return self._parent
@parent.setter
def parent(self, p):
if p is None:
del self.parent
else:
self.set_parent(p)
@parent.deleter
def parent(self):
self._parent = None
def set_parent(self, p):
"""Set the parent `Tab` for this tab
Parameters
----------
p : `Tab`
the parent tab for this one
"""
if p and self._children:
raise ValueError("A tab cannot have both a parent, and a "
"set of children.")
if isinstance(p, BaseTab) and self not in p.children:
p.add_child(self)
else:
p = ParentTab(p, [self], mode=self.mode)
self._parent = p
@property
def children(self):
"""List of child tabs for this `Tab`
If this tab is given children, it cannot also have a parent, as it
will define its own dropdown menu in the HTML navigation bar, linking
to itself and its children.
:type: `list` of `tabs <Tab>`
"""
return self._children
@children.setter
def children(self, clist):
if self._parent and clist:
raise ValueError("A Tab cannot have both a parent, and a "
"set of children.")
self._children = list(clist)
@property
def index(self):
"""The HTML path (relative to the `~Tab.path`) for this tab
"""
if not self._index:
if self.shortname.lower() == 'summary':
p = ''
else:
p = re_cchar.sub('_', self.shortname.strip('_')).lower()
tab_ = self
while tab_.parent:
p = os.path.join(re_cchar.sub(
'_', tab_.parent.shortname.strip('_')).lower(), p)
tab_ = tab_.parent
self._index = os.path.normpath(os.path.join(
self.path, p, 'index.html'))
return self._index
@index.setter
def index(self, p):
self._index = p
@index.deleter
def index(self):
self._index = None
@property
def href(self):
"""HTML href (relative to the `~Tab.path`) for this tab
This attribute is just a convenience to clean-up the
`~Tab.index` for a given tab, by removing index.htmls.
hierarchy.
:type: `str`
"""
if os.path.basename(self.index) in ('index.html', 'index.php'):
return os.path.split(self.index)[0] + os.path.sep
elif self.index:
return self.index
else:
return ''
@property
def title(self):
"""Page title for this tab
"""
if self.parent:
title = self.name.strip('_')
tab_ = self
while tab_.parent:
title = '%s/%s' % (tab_.parent.name.strip('_'), title)
tab_ = tab_.parent
return title
else:
return self.name
@property
def shorttitle(self):
"""Page title for this tab
"""
if self.parent:
title = self.shortname.strip('_')
tab_ = self
while tab_.parent:
title = '%s/%s' % (tab_.parent.shortname.strip('_'), title)
tab_ = tab_.parent
return title
else:
return self.shortname.strip('_')
@property
def group(self):
"""Dropdown group for this `Tab` in the navigation bar
:type: `str`
"""
return self._group
@group.setter
def group(self, gp):
if gp is None:
self._group = None
else:
self._group = str(gp)
@property
def notes(self):
"""Release notes for this `Tab`
"""
return self._notes
@notes.setter
def notes(self, n):
if n is None:
self._notes = None
else:
self._notes = n
@property
def overlay(self):
"""Boolean switch to enable plot overlay for this `Tab`
"""
return self._overlay
@overlay.setter
def overlay(self, ovl):
self._overlay = ovl
@property
def mode(self):
"""The date-time mode of this tab.
:type: `int`
See Also
--------
gwsumm.mode : for details on the modes
"""
return self._mode
@mode.setter
def mode(self, m):
self._mode = get_mode(m)
@mode.deleter
def mode(self):
self._mode = get_mode(0)
# -- Tab instance methods -------------------
def add_child(self, tab):
"""Add a child to this `SummaryTab`
Parameters
----------
tab : `SummaryTab`
child tab to record
"""
self.children.append(tab)
def get_child(self, name):
"""Find a child tab of this `SummaryTab` by name
Parameters
----------
name : `str`
string identifier of child tab to use in search
Returns
-------
child : `SummaryTab`
the child tab found by name
Raises
------
RuntimeError
if no child tab can be found matching the given ``name``
"""
names = [c.name for c in self.children]
try:
idx = names.index(name)
except ValueError:
raise RuntimeError("This tab has no child named '%s'." % name)
else:
return self.children[idx]
# -- Tab configuration parser ---------------
@classmethod
def from_ini(cls, cp, section, *args, **kwargs):
"""Define a new tab from a `~gwsumm.config.GWConfigParser`
Parameters
----------
cp : `~gwsumm.config.GWConfigParser`
customised configuration parser containing given section
section : `str`
name of section to parse
*args, **kwargs
other positional and keyword arguments to pass to the class
constructor (`__init__`)
Returns
-------
tab : `Tab`
a new tab defined from the configuration
Notes
-----
This method parses the following configuration options
.. autosummary::
~Tab.name
~Tab.shortname
~Tab.parent
~Tab.group
~Tab.notes
~Tab.overlay
~Tab.index
Sub-classes should parse their own configuration values and then pass
these as ``*args`` and ``**kwargs`` to this method via `super`:
.. code-block:: python
class MyTab(Tab):
[...]
def from_ini(cls, cp, section)
\"\"\"Define a new `MyTab`.
\"\"\"
foo = cp.get(section, 'foo')
bar = cp.get(section, 'bar')
return super(MyTab, cls).from_ini(cp, section, foo, bar=bar)
"""
# get tab name
try:
# name given explicitly
name = re_quote.sub('', cp.get(section, 'name'))
except NoOptionError:
# otherwise strip 'tab-' from section name
name = section[4:]
try:
kwargs.setdefault('shortname',
re_quote.sub('', cp.get(section, 'shortname')))
except NoOptionError:
pass
# get parent:
# if parent is not given, this assumes a top-level tab
try:
kwargs.setdefault('parent',
re_quote.sub('', cp.get(section, 'parent')))
except NoOptionError:
pass
else:
if kwargs['parent'] == 'None':
kwargs['parent'] = None
# get group
try:
kwargs.setdefault('group', cp.get(section, 'group'))
except NoOptionError:
pass
# get HTML file
try:
kwargs.setdefault('index', cp.get(section, 'index'))
except NoOptionError:
pass
# get hidden param
try:
hidden = cp.get(section, 'hidden')
except NoOptionError:
hidden = False
else:
if hidden is None:
hidden = True
else:
hidden = bool(hidden.title())
kwargs.setdefault('hidden', hidden)
# get release notes
try:
kwargs.setdefault('notes', cp.get(section, 'notes'))
except NoOptionError:
pass
# determine whether plot overlay is requested
try:
overlay = cp.get(section, 'overlay')
except NoOptionError:
overlay = True
else:
if overlay is None:
overlay = True
else:
overlay = bool(overlay.title())
kwargs.setdefault('overlay', overlay)
# get mode and times if required
try:
kwargs['mode']
except KeyError:
try:
kwargs['mode'] = get_mode(cp.get(section, 'mode'))
except NoOptionError:
kwargs['mode'] = get_mode()
if isinstance(kwargs['mode'], str):
kwargs['mode'] = get_mode(kwargs['mode'])
if kwargs['mode'] >= Mode.gps:
try:
kwargs['start']
except KeyError:
kwargs['start'] = cp.getint(section, 'gps-start-time')
try:
kwargs['end']
except KeyError:
kwargs['end'] = cp.getint(section, 'gps-end-time')
elif kwargs['mode'] == Mode.event:
try:
kwargs['gpstime']
except KeyError:
kwargs['gpstime'] = cp.getfloat(section, 'gpstime')
try:
kwargs['duration']
except KeyError:
kwargs['duration'] = cp.getfloat(section, 'duration')
return cls(name, *args, **kwargs)
# -- HTML operations ------------------------
# the following related HTML operations are defined here
#
# - navbar: create navbar
# - banner: create header
# - content: create main content
#
# The `Tab.write_html` method pulls all of these things together and
# is the primary user-facing HTML method
def html_banner(self, title=None, subtitle=None):
"""Build the HTML headline banner for this tab.
Parameters
----------
title : `str`
title for this page
subtitle : `str`
sub-title for this page
Returns
-------
banner : `~MarkupPy.markup.page`
formatter markup page for the banner
"""
# work title as Parent Name/Tab Name
if title is None and subtitle is None:
title = self.title.replace('/', ' : ', 1)
return html.banner(title, subtitle=subtitle)
def html_navbar(self, help_=None, calendar=[], tabs=list(),
ifo=None, ifomap=dict(), **kwargs):
"""Build the navigation bar for this tab.
Parameters
----------
help_ : `str`, `~MarkupPy.markup.page`
content to place on the upper-right side of the navbar
calendar : `list`, optional
datepicker calendar objects for navigation
tabs : `list`, optional
list of parent tabs (each with a list of children) to include
in the navigation bar.
ifo : `str`, optional
prefix for this IFO.
ifomap : `dict`, optional
`dict` of (ifo, {base url}) pairs to map to summary pages for
other IFOs.
**kwargs
other keyword arguments to pass to :meth:`gwsumm.html.navbar`
Returns
-------
page : `~MarkupPy.markup.page`
a markup page containing the navigation bar.
"""
class_ = 'navbar fixed-top navbar-expand-md shadow-sm'
# build interferometer cross-links
if ifo is not None:
brand_ = html.base_map_dropdown(ifo, id_='ifos', bases=ifomap)
class_ += ' navbar-%s' % ifo.lower()
else:
brand_ = markup.page()
# build HTML brand
if help_:
brand_ = (brand_, help_)
# build tabs and calendar
tabs = self._html_navbar_links(tabs)
if calendar:
tabs = list(calendar) + tabs
# combine and return
return gwhtml.navbar(tabs, class_=class_, brand=brand_, **kwargs)
def _html_navbar_links(self, tabs):
"""Construct the ordered list of tabs to write into the navbar
Parameters
----------
tabs : `list`
a list of `Tabs <gwsumm.tabs.Tab`, some of which may contain
`~Tab.children`.
Returns
-------
links : `str`
a structured list of navigation menu entries. Each element will
be a (`name`, `entries`) tuple defining the heading and dropdown
entries for a single dropdown menu. The `entries` list will again
be a list of (`name`, (`link`|`entries`)) tuples, each defining
a name and link for a given entry in a single dropdown menu,
or a set of (`name`, `link`) tuples for a group in a dropdown
menu.
"""
# build navbar links
navlinks = []
tabs = TabList(tabs).get_hierarchy()
for tab in tabs:
if tab.hidden:
continue
children = [t for t in tab.children if not t.hidden]
if len(children):
navlinks.append([tab.shortname.strip('_'), []])
links = []
active = None
# build groups
groups = set([t.group for t in children])
groups = dict((g, [t for t in children if t.group == g])
for g in groups)
nogroup = sorted(
groups.pop(None, []),
key=(lambda c:
c.shortname.lower() in ['summary', 'overview'] and
c.shortname.upper() or c.shortname.lower()))
for child in nogroup:
links.append((child.shortname.strip('_'), child.href))
if child == self:
active = len(links) - 1
for group in sorted(list(groups)):
# sort group by name
re_group = re.compile(
r'(\A{0}\s|\s{0}\Z)'.format(group.strip('_')),
re.I,
)
names = [re_group.sub('', t.shortname)
for t in groups[group]]
groups[group] = list(zip(*sorted(
list(zip(groups[group], names)),
key=(lambda x:
x[1].lower() in ['summary', 'overview'] and
' %s' % x[1].upper() or x[1].lower()),
)))[0]
# build link sets
links.append((group.strip('_'), []))
for i, child in enumerate(groups[group]):
name = re_group.sub('', child.shortname.strip('_'))
links[-1][1].append((name, child.href))
if child == self:
active = [len(links) - 1, i]
if (children[0].shortname == 'Summary' and
not children[0].group and len(children)) > 1:
links.insert(1, None)
if active and isinstance(active, int) and active > 0:
active += 1
elif active and isinstance(active, list) and active[0] > 0:
active[0] += 1
navlinks[-1][1].extend(links)
navlinks[-1].append(active)
else:
navlinks.append((tab.shortname.strip('_'), tab.href))
return navlinks
@staticmethod
def html_content(content):
"""Build the #main div for this tab.
Parameters
----------
content : `str`, `~MarkupPy.markup.page`
HTML content to be wrapped
Returns
-------
#main : `~MarkupPy.markup.page`
A new `page` with the input content wrapped as
"""
page = markup.page()
page.div(id_='main')
page.div(str(content), id_='content')
page.div.close()
return page
def write_html(self, maincontent, title=None, subtitle=None, tabs=list(),
ifo=None, ifomap=dict(), help_=None, base=None, css=None,
js=None, about=None, footer=None, issues=True, **inargs):
"""Write the HTML page for this `Tab`.
Parameters
----------
maincontent : `str`, `~MarkupPy.markup.page`
simple string content, or a structured `page` of markup to
embed as the content of the #main div.
title : `str`, optional, default: {parent.name}
level 1 heading for this `Tab`.
subtitle : `str`, optional, default: {self.name}
level 2 heading for this `Tab`.
tabs: `list`, optional
list of top-level tabs (with children) to populate navbar
ifo : `str`, optional
prefix for this IFO.
ifomap : `dict`, optional
`dict` of (ifo, {base url}) pairs to map to summary pages for
other IFOs.
help_ : `str`, `~MarkupPy.markup.page`, optional
non-menu content for navigation bar
css : `list`, optional
list of resolvable URLs for CSS files. See `gwsumm.html.CSS` for
the default list.
js : `list`, optional
list of resolvable URLs for javascript files. See
`gwumm.html.JS` for the default list.
about : `str`, optional
href for the 'About' page
footer : `str`, `~MarkupPy.markup.page`
external link, if applicable (linked from an icon in the footer)
issues : `bool` or `str`, default: `True`
print link to github.com issue tracker for this package
**inargs
other keyword arguments to pass to the
:meth:`~Tab.build_inner_html` method
"""
# setup directories
outdir = os.path.split(self.index)[0]
if outdir and not os.path.isdir(outdir):
os.makedirs(outdir)
# get default style and scripts
if css is None:
css = list(html.get_css().values())
if js is None:
js = list(html.get_js().values())
# find relative base path
if base is None:
n = len(self.index.split(os.path.sep)) - 1
base = os.sep.join([os.pardir] * n)
if not base.endswith('/'):
base += '/'
# set default title
if title is None:
title = self.shorttitle.replace('/', ' | ')
# construct navigation
if tabs:
navbar = str(self.html_navbar(ifo=ifo, ifomap=ifomap,
tabs=tabs, help_=help_))
else:
navbar = None
# initialize page
self.page = gwhtml.new_bootstrap_page(
title=title, base=base, path=self.path,
css=css, script=js, navbar=navbar)
# add banner
self.page.add(str(self.html_banner(
title=title.replace('|', ':'), subtitle=subtitle)))
# add help button
if self.notes is not None:
self.page.add(str(html.dialog_box(
self.notes, title='Help', id_='help',
btntxt=markup.oneliner.i('', class_='fas fa-question'))))
# add overlay button
if self.overlay:
self.page.add(str(html.overlay_canvas()))
# add #main content
self.page.add(str(self.html_content(maincontent)))
# format custom footer
version = get_version()
url = f'https://github.com/gwpy/gwsumm/releases/tag/{version}'
# close page and write
gwhtml.close_page(self.page, self.index, about=about,
link=(f'gwsumm-{version}', url, 'GitHub'),
issues=issues, external=footer)
return
# -- Mixins -------------------------------------------------------------------
#
# All actual `Tab` objects will come in three favours:
#
# - `StaticTab` - no GPS associations
# - `IntervalTab` - associated with GPS start and stop time
# - `EventTab` - associated with central GPS time (and duration)
class StaticTab(BaseTab):
"""Simple `Tab` with no GPS association
"""
pass
class GpsTab(BaseTab):
"""Stub for GPS-related tabs
"""
@property
def span(self):
"""The GPS [start, end) span of this tab.
:type: `~gwpy.segments.Segment`
"""
return self._span
@span.setter
def span(self, seg):
if seg:
self._span = Segment(*map(to_gps, seg))
else:
self._span = None
@property
def start(self):
"""The GPS start time of this tab.
:type: `float`
"""
try:
return self.span[0]
except TypeError:
return None
@property
def end(self):
"""The GPS end time of this tab.
:type: `float`
"""
try:
return self.span[1]
except TypeError:
return None
class IntervalTab(GpsTab):
"""`Tab` defined within a GPS [start, end) interval
"""
def __init__(self, *args, **kwargs):
try:
span = kwargs.pop('span')
except KeyError:
try:
start = kwargs.pop('start')
end = kwargs.pop('end')
except KeyError:
mode = get_mode(kwargs.get('mode')).name
raise TypeError("%s() in %r mode needs keyword argument 'span'"
" or both 'start' and 'end'"
% (type(self).__name__, mode))
else:
span = (start, end)
self.span = span
super(IntervalTab, self).__init__(*args, **kwargs)
def html_calendar(self):
"""Build the datepicker calendar for this tab.
Notes
-----
The datetime for the calendar is taken from this tab's `~GpsTab.span`
"""
date = from_gps(self.start)
# double-check path matches what is required from custom datepicker
try:
requiredpath = get_base(date, mode=self.mode)
except ValueError:
return ['%d-%d' % (self.start, self.end)]
if requiredpath not in self.path:
raise RuntimeError("Tab path %r inconsistent with required "
"format including %r for archive calendar"
% (self.path, requiredpath))
# format calendar
return html.calendar(date, mode=self.mode)
def html_navbar(self, help_=None, calendar=True, **kwargs):
"""Build the navigation bar for this `Tab`.
The navigation bar will consist of a switch for this page linked
to other interferometer servers, followed by the navbar brand,
then the full dropdown-based navigation menus configured for the
given ``tabs`` and their descendents.
Parameters
----------
help_ : `str`, `~MarkupPy.markup.page`
content for upper-right of navbar
ifo : `str`, optional
prefix for this IFO.
ifomap : `dict`, optional
`dict` of (ifo, {base url}) pairs to map to summary pages for
other IFOs.
tabs : `list`, optional
list of parent tabs (each with a list of children) to include
in the navigation bar.
Returns
-------
page : `~MarkupPy.markup.page`
a markup page containing the navigation bar.
"""
# add calendar
calendar = calendar and self.html_calendar()
# combine and return
return super(IntervalTab, self).html_navbar(
help_=help_, calendar=calendar, **kwargs)
class EventTab(GpsTab):
"""`Tab` defined around a central GPS time
"""
@property
def gpstime(self):
"""Central GPS time of this tab
:type: `~gwpy.time.LIGOTimeGPS`
"""
return self._gpstime
@gpstime.setter
def gpstime(self, t):
self._gpstime = to_gps(t)
@property
def datetime(self):
return from_gps(self.gpstime)
@property
def duration(self):
"""Time duration of this tab, centred on the `gpstime`
:type: `float`
"""
return abs(self.span)
@duration.setter
def duration(self, d):
d2 = d/2.
self.span = (self.gpstime - d2, self.gpstime + d2)
# -- EventTab methods -----------------------
def __init__(self, *args, **kwargs):
# parse gpstime and duration
try:
gpstime = kwargs.pop('gpstime')
except KeyError:
mode = get_mode(kwargs['mode']).name
raise TypeError("%s() in %r mode needs keyword argument 'gpstime'"
% (type(self).__name__, mode))
duration = kwargs.pop('duration', 200)
self.gpstime = gpstime
self.duration = duration
# create tab and assign properties
super(EventTab, self).__init__(*args, **kwargs)
def html_navbar(self, calendar=[], **kwargs):
if not calendar:
calendar = str(self.gpstime)
super(EventTab, self).html_navbar(calendar=calendar, **kwargs)
html_navbar.__doc__ = GpsTab.html_navbar.__doc__
class _MetaTab(type):
"""Metaclass for creating tabs of the right flavour
The `MetaTab.__call__` method will get executed whenever a subclass of
`Tab` is created. It works out the 'mode' of the new tab and dynamically
sets the parent class for `Tab` accordingly.
"""
def __call__(cls, *args, **kwargs):
"""Parse the `mode` kwarg for the Tab and add the right flavour
"""
# parse default mode based on other kwargs
try:
mode = kwargs['mode']
except KeyError:
if 'gpstime' in kwargs:
mode = 'EVENT'
else:
mode = None
mode = get_mode(mode)
# parse regular Tab (don't add a mixin)
if mode == Mode.static:
kwargs.pop('mode', None)
base = StaticTab
# parse event Tab
elif mode == Mode.event:
kwargs['mode'] = mode
base = EventTab
# parse interval Tab
else:
kwargs['mode'] = mode
base = IntervalTab
# set bases and create Tab
Tab.__bases__ = (base,)
return super(_MetaTab, cls).__call__(*args, **kwargs)
# -- Tab ----------------------------------------------------------------------
# this is the first actual Tab object, all of the functionality is defined
# in the `BaseTab` object
class Tab(BaseTab, metaclass=_MetaTab):
"""A Simple HTML tab.
This `class` provides a mechanism to generate a full-formatted
HTML page including banner, navigation-bar, content, and a footer,
without the user worrying too much about the details.
For example::
>>> # import Tab and make a new one with a given title and HTML file
>>> from gwsumm.tabs import Tab
>>> tab = Tab('My new tab', 'mytab.html')
>>> # write the Tab to disk with some simple content
>>> tab.write_html('This is my content', brand='Brand name')
Parameters
----------
name : `str`
name of this tab (required)
index : `str`
HTML file in which to write. By default each tab is written to
an index.html file in its own directory. Use `~Tab.index`
to find out the default index, if not given.
shortname : `str`
shorter name for this tab to use in the navigation bar. By
default the regular name is used
parent : `~gwsumm.tabs.Tab`
parent of this tab. This is used to position this tab in the
navigation bar.
children : `list`
list of child `Tabs <~gwsumm.tabs.Tab>` of this one. This
is used to position this tab in the navigation bar.
group : `str`
name of containing group for this tab in the navigation bar
dropdown menu. This is only relevant if this tab has a parent.
path : `str`
base output directory for this tab (should be the same directory
for all tabs in this run)
Notes
-----
A `Tab` cannot have both a `~Tab.parent` and `~tab.Children`.
This is a limitation imposed by the twitter bootstrap navigation bar
implementation, which does not allow nested dropdown menus. In order
to collect child tabs in a given place, assign them all the same
`~Tab.group`.
"""
type = 'basic'
register_tab(Tab)
class ParentTab(Tab):
"""Dummy `Tab` only for navigation
"""
def __init__(self, name, children, **kwargs):
# parse list of children
if not isinstance(children, list):
children = [children]
child = children[0]
# parse mode and GPS arguments (if required)
kwargs.setdefault('mode', child.mode)
if isinstance(self, EventTab):
kwargs.setdefault('gpstime', child.gpstime)
kwargs.setdefault('duration', child.duration)
elif isinstance(self, IntervalTab):
kwargs.setdefault('span', child.span)
# create Tab
super(ParentTab, self).__init__(name, children=children, **kwargs)
# -- TabList -----------------------------------------------------------------
class TabList(list):
"""Custom `list` of `Tab` objects with sorting and parsing
"""
def __init__(self, entries=[]):
super(TabList, self).__init__(entries)
def get_hierarchy(self):
parents = OrderedDict()
# 1. Assume all tabs without parents are parents themselves
for tab in [tab for tab in self if tab.parent is None]:
parents[tab.name] = tab
# 2. All remaining tabs without a defined parent define that parent
# 3. Sort all tabs into their parent sets
for tab in [tab for tab in self if tab.parent is not None]:
if tab.parent in parents:
tab.set_parent(parents[tab.parent])
elif not isinstance(tab.parent, Tab):
tab.set_parent(get_tab('default')(
tab.parent, mode=tab.mode, span=tab.span))
parents.setdefault(tab.parent.name, tab.parent)
if tab not in tab.parent.children:
tab.parent.add_child(tab)
return list(parents.values())
@staticmethod
def _sortkey(tab):
# NOTE: we need all return values to be strings for
# the sorting to actually work
if 'Home' in tab.shortname:
return '1'
if tab.shortname == 'Summary' and tab.parent is None:
return '2'
if tab.shortname == 'Summary':
return '3'
if 'ODC' in tab.shortname:
return '4'
if tab.shortname.islower():
return tab.shortname.upper()
return tab.shortname.lower()
def sort(self, key=None, reverse=False):
"""Sort this `TabList` in place
"""
if key is None:
key = self._sortkey
hlist = sorted(self.get_hierarchy(), key=key)
for tab in hlist:
tab.children.sort(key=key)
super(TabList, self).sort(key=key, reverse=reverse)
@classmethod
def from_ini(cls, config, tag='tab[_-]', match=[],
path=os.curdir, plotdir='plots'):
if isinstance(tag, str):
tag = re.compile(tag)
tabs = cls()
parents = {}
for section in filter(tag.match, config.sections()):
# if user gave matches, test match and skip
if match and section[4:] not in match:
continue
# otherwise, get type and create instance of class
try:
type_ = config.get(section, 'type')
except NoOptionError:
type_ = 'default'
Tab = get_tab(type_)
if issubclass(Tab, get_tab('data')):
tab = Tab.from_ini(config, section, plotdir=plotdir, path=path)
else:
tab = Tab.from_ini(config, section, path=path)
tabs.append(tab)
if tab.parent and tab.parent.name in parents:
tab.set_parent(parents[tab.parent.name])
elif tab.parent:
parents[tab.parent.name] = tab.parent
tabs.get_hierarchy() # call this to resolve map parent names to tabs
return tabs