Source code for gwsumm.tabs.core

# 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

[docs] 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
[docs] 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 -------------------
[docs] def add_child(self, tab): """Add a child to this `SummaryTab` Parameters ---------- tab : `SummaryTab` child tab to record """ self.children.append(tab)
[docs] 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 ---------------
[docs] @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
[docs] 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)
[docs] 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
[docs] @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
[docs] 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
[docs] 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 -----------------------------------------------------------------
[docs] class TabList(list): """Custom `list` of `Tab` objects with sorting and parsing """ def __init__(self, entries=[]): super(TabList, self).__init__(entries)
[docs] 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()
[docs] 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)
[docs] @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