예제 #1
0
class RcParamsWidget(ConfigPage, QWidget):
    """A configuration page for RcParams instances

    This page displays the :class:`psyplot.config.rcsetup.RcParams` instance in
    the :attr:`rc` attribute and let's the user modify it.

    Notes
    -----
    After the initialization, you have to call the :meth:`initialize` method"""

    #: the rcParams to use (must be implemented by subclasses)
    rc = None

    #: the :class:`RcParamsTree` that is used to display the rcParams
    tree = None

    @property
    def propose_changes(self):
        """A signal that is emitted if the user changes the values in the
        rcParams"""
        return self.tree.propose_changes

    @property
    def validChanged(self):
        """A signal that is emitted if the user changes the valid state of this
        page"""
        return self.tree.validChanged

    @property
    def changed(self):
        """True if any changes are proposed by this config page"""
        return bool(next(self.tree.changed_rc(), None))

    @property
    def is_valid(self):
        """True if all the settings are valid"""
        return self.tree.is_valid

    @property
    def icon(self):
        """The icon of this instance in the :class:`Preferences` dialog"""
        return QIcon(get_icon('rcParams.png'))

    def __init__(self, *args, **kwargs):
        super(RcParamsWidget, self).__init__(*args, **kwargs)
        self.vbox = vbox = QVBoxLayout()

        self.description = QLabel(
            '<p>Modify the rcParams for your need. Changes will not be applied'
            ' until you click the Apply or Ok button.</p>'
            '<p>Values must be entered in yaml syntax</p>', parent=self)
        vbox.addWidget(self.description)
        self.tree = tree = RcParamsTree(
            self.rc, getattr(self.rc, 'validate', None),
            getattr(self.rc, 'descriptions', None), parent=self)
        tree.setSelectionMode(QAbstractItemView.MultiSelection)
        vbox.addWidget(self.tree)

        self.bt_select_all = QPushButton('Select All', self)
        self.bt_select_changed = QPushButton('Select changes', self)
        self.bt_select_none = QPushButton('Clear Selection', self)
        self.bt_export = QToolButton(self)
        self.bt_export.setText('Export Selection...')
        self.bt_export.setToolTip('Export the selected rcParams to a file')
        self.bt_export.setPopupMode(QToolButton.InstantPopup)
        self.export_menu = export_menu = QMenu(self)
        export_menu.addAction(self.save_settings_action())
        export_menu.addAction(self.save_settings_action(True))
        self.bt_export.setMenu(export_menu)
        hbox = QHBoxLayout()
        hbox.addWidget(self.bt_select_all)
        hbox.addWidget(self.bt_select_changed)
        hbox.addWidget(self.bt_select_none)
        hbox.addStretch(1)
        hbox.addWidget(self.bt_export)
        vbox.addLayout(hbox)

        self.setLayout(vbox)

        self.bt_select_all.clicked.connect(self.tree.selectAll)
        self.bt_select_none.clicked.connect(self.tree.clearSelection)
        self.bt_select_changed.clicked.connect(self.tree.select_changes)

    def save_settings_action(self, update=False, target=None):
        """Create an action to save the selected settings in the :attr:`tree`

        Parameters
        ----------
        update: bool
            If True, it is expected that the file already exists and it will be
            updated. Otherwise, existing files will be overwritten
        """
        def func():
            if update:
                meth = QFileDialog.getOpenFileName
            else:
                meth = QFileDialog.getSaveFileName
            if target is None:
                fname = meth(
                    self, 'Select a file to %s' % (
                        'update' if update else 'create'),
                    self.default_path,
                    'YAML files (*.yml);;'
                    'All files (*)'
                    )
                if with_qt5:  # the filter is passed as well
                    fname = fname[0]
            else:
                fname = target
            if not fname:
                return
            if update:
                rc = self.rc.__class__(defaultParams=self.rc.defaultParams)
                rc.load_from_file(fname)
                old_keys = list(rc)
                selected = dict(self.tree.selected_rc())
                new_keys = list(selected)
                rc.update(selected)
                rc.dump(fname, include_keys=old_keys + new_keys,
                        exclude_keys=[])
            else:
                rc = self.rc.__class__(self.tree.selected_rc(),
                                       defaultParams=self.rc.defaultParams)
                rc.dump(fname, exclude_keys=[])

        action = QAction('Update...' if update else 'Overwrite...', self)
        action.triggered.connect(func)
        return action

    def initialize(self, rcParams=None, validators=None, descriptions=None):
        """Initialize the config page

        Parameters
        ----------
        rcParams: dict
            The rcParams to use. If None, the :attr:`rc` attribute of this
            instance is used
        validators: dict
            A mapping from the `rcParams` key to the corresponding validation
            function for the value. If None, the
            :attr:`~psyplot.config.rcsetup.RcParams.validate` attribute of the
            :attr:`rc` attribute is used
        descriptions: dict
            A mapping from the `rcParams` key to it's description. If None, the
            :attr:`~psyplot.config.rcsetup.RcParams.descriptions` attribute of
            the :attr:`rc` attribute is used"""
        if rcParams is not None:
            self.rc = rcParams
            self.tree.rc = rcParams
        if validators is not None:
            self.tree.validators = validators
        if descriptions is not None:
            self.tree.descriptions = descriptions
        self.tree.initialize()

    def apply_changes(self):
        """Apply the changes in the config page"""
        self.tree.apply_changes()
예제 #2
0
class UrlHelp(UrlBrowser, HelpMixin):
    """Class to convert rst docstrings to html and show browsers"""

    #: Object containing the necessary fields to describe an object given to
    #: the help widget. The descriptor is set up by the :meth:`describe_object`
    #: method and contains an additional objtype attribute
    object_descriptor = namedtuple('ObjectDescriptor',
                                   ['obj', 'name', 'objtype'])

    can_document_object = with_sphinx
    can_show_rst = with_sphinx

    #: menu button with different urls
    bt_url_menus = None

    #:
    sphinx_thread = None

    def __init__(self, *args, **kwargs):
        self._temp_dir = 'sphinx_dir' not in kwargs
        self.sphinx_dir = kwargs.pop('sphinx_dir', mkdtemp(prefix='psyplot_'))
        self.build_dir = osp.join(self.sphinx_dir, '_build', 'html')
        super(UrlHelp, self).__init__(*args, **kwargs)

        self.error_msg = PyErrorMessage(self)
        if with_sphinx:
            self.sphinx_thread = SphinxThread(self.sphinx_dir)
            self.sphinx_thread.html_ready[str].connect(self.browse)
            self.sphinx_thread.html_error[str].connect(
                self.error_msg.showTraceback)
            self.sphinx_thread.html_error[str].connect(logger.debug)
            rcParams.connect('help_explorer.render_docs_parallel',
                             self.reset_sphinx)
            rcParams.connect('help_explorer.use_intersphinx',
                             self.reset_sphinx)
            rcParams.connect('help_explorer.online', self.reset_sphinx)

        self.bt_connect_console = QToolButton(self)
        self.bt_connect_console.setCheckable(True)
        if rcParams['console.connect_to_help']:
            self.bt_connect_console.setIcon(
                QIcon(get_icon('ipython_console.png')))
            self.bt_connect_console.click()
        else:
            self.bt_connect_console.setIcon(
                QIcon(get_icon('ipython_console_t.png')))
        self.bt_connect_console.clicked.connect(self.toogle_connect_console)
        rcParams.connect('console.connect_to_help',
                         self.update_connect_console)
        self.toogle_connect_console()

        # menu button with different urls
        self.bt_url_menus = QToolButton(self)
        self.bt_url_menus.setIcon(QIcon(get_icon('docu_button.png')))
        self.bt_url_menus.setToolTip('Browse documentations')
        self.bt_url_menus.setPopupMode(QToolButton.InstantPopup)

        docu_menu = QMenu(self)
        for name, url in six.iteritems(self.doc_urls):

            def to_url(b, url=url):
                self.browse(url)

            action = QAction(name, self)
            action.triggered.connect(to_url)
            docu_menu.addAction(action)
        self.bt_url_menus.setMenu(docu_menu)

        self.button_box.addWidget(self.bt_connect_console)
        self.button_box.addWidget(self.bt_url_menus)
        # toogle the lock again to set the bt_url_menus enabled state
        self.toogle_url_lock()

    def update_connect_console(self, connect):
        if (connect and not self.bt_connect_console.isChecked()
                or not connect and self.bt_connect_console.isChecked()):
            self.bt_connect_console.click()

    def toogle_connect_console(self):
        """Disable (or enable) the loading of web pages in www"""
        bt = self.bt_connect_console
        connect = bt.isChecked()
        bt.setIcon(
            QIcon(
                get_icon('ipython_console.png'
                         if connect else 'ipython_console_t.png')))
        bt.setToolTip("%sonnect the console to the help explorer" %
                      ("Don't c" if connect else "C"))
        if rcParams['console.connect_to_help'] is not connect:
            rcParams['console.connect_to_help'] = connect

    def reset_sphinx(self, value):
        """Method that is called if the configuration changes"""
        if with_sphinx and hasattr(self.sphinx_thread, 'app'):
            del self.sphinx_thread.app

    @docstrings.dedent
    def show_help(self, obj, oname='', files=None):
        """
        Render the rst docu for the given object with sphinx and show it

        Parameters
        ----------
        %(HelpMixin.show_help.parameters)s
        """
        if self.bt_lock.isChecked():
            return
        return super(UrlHelp, self).show_help(obj, oname=oname, files=files)

    @docstrings.dedent
    def show_intro(self, text=''):
        """
        Show the intro text in the explorer

        Parameters
        ----------
        %(HelpMixin.show_intro.parameters)s"""
        if self.sphinx_thread is not None:
            with open(self.sphinx_thread.index_file, 'a') as f:
                f.write('\n' + text.strip() + '\n\n' + 'Table of Contents\n'
                        '=================\n\n.. toctree::\n')
            self.sphinx_thread.render(None, None)

    def show_rst(self, text, oname='', descriptor=None, files=None):
        """Render restructured text with sphinx and show it

        Parameters
        ----------
        %(HelpMixin.show_rst.parameters)s"""
        if self.bt_lock.isChecked() or self.sphinx_thread is None:
            return False
        if not oname and descriptor:
            oname = descriptor.name
        for f in files or []:
            shutil.copyfile(f, osp.join(self.sphinx_dir, osp.basename(f)))
        self.sphinx_thread.render(text, oname)
        return True

    def describe_object(self, obj, oname=''):
        """Describe an object using additionaly the object type from the
        :meth:`get_objtype` method

        Returns
        -------
        instance of :attr:`object_descriptor`
            The descriptor of the object"""
        return self.object_descriptor(obj, oname, self.get_objtype(obj))

    def browse(self, url):
        """Reimplemented to add file paths to the url string"""
        url = asstring(url)
        html_file = osp.join(self.sphinx_dir, '_build', 'html', url + '.html')
        if osp.exists(html_file):
            url = file2html(html_file)
        super(UrlHelp, self).browse(url)

    def toogle_url_lock(self):
        """Disable (or enable) the loading of web pages in www"""
        super(UrlHelp, self).toogle_url_lock()
        # enable or disable documentation button
        bt = self.bt_url_lock
        offline = bt.isChecked()
        try:
            self.bt_url_menus.setEnabled(not offline)
        except AttributeError:  # not yet initialized
            pass

    def url_changed(self, url):
        """Reimplemented to remove file paths from the url string"""
        try:
            url = asstring(url.toString())
        except AttributeError:
            pass
        if url.startswith('file://'):
            fname = html2file(url)
            if osp.samefile(self.build_dir,
                            osp.commonprefix([fname, self.build_dir])):
                url = osp.splitext(osp.basename(fname))[0]
        super(UrlHelp, self).url_changed(url)

    def header(self, descriptor, sig):
        return '%(name)s\n%(bars)s\n\n.. py:%(type)s:: %(name)s%(sig)s\n' % {
            'name': descriptor.name,
            'bars': '-' * len(descriptor.name),
            'type': descriptor.objtype,
            'sig': sig
        }

    def get_objtype(self, obj):
        """Get the object type of the given object and determine wheter the
        object is considered a class, a module, a function, method or data

        Parameters
        ----------
        obj: object

        Returns
        -------
        str
            One out of {'class', 'module', 'function', 'method', 'data'}"""
        if inspect.isclass(obj):
            return 'class'
        if inspect.ismodule(obj):
            return 'module'
        if inspect.isfunction(obj) or isinstance(obj, type(all)):
            return 'function'
        if inspect.ismethod(obj) or isinstance(obj, type(str.upper)):
            return 'method'
        return 'data'

    def is_importable(self, modname):
        """Determine whether members of the given module can be documented with
        sphinx by using the :func:`sphinx.util.get_module_source` function

        Parameters
        ----------
        modname: str
            The __name__ attribute of the module to import

        Returns
        -------
        bool
            True if sphinx can import the module"""
        try:
            get_module_source(modname)
            return True
        except Exception:
            return False

    def get_doc(self, descriptor):
        """Reimplemented to (potentially) use the features from
        sphinx.ext.autodoc"""
        obj = descriptor.obj
        if inspect.ismodule(obj):
            module = obj
        else:
            module = inspect.getmodule(obj)
        if module is not None and (re.match('__.*__', module.__name__)
                                   or not self.is_importable(module.__name__)):
            module = None
        isclass = inspect.isclass(obj)
        # If the module is available, we try to use autodoc
        if module is not None:
            doc = '.. currentmodule:: ' + module.__name__ + '\n\n'
            # a module --> use automodule
            if inspect.ismodule(obj):
                doc += self.header(descriptor, '')
                doc += '.. automodule:: ' + obj.__name__
            # an importable class --> use autoclass
            elif isclass and getattr(module, obj.__name__, None) is not None:
                doc += self.header(descriptor, '')
                doc += '.. autoclass:: ' + obj.__name__
            # an instance and the class can be imported
            # --> use super get_doc and autoclass for the tyoe
            elif descriptor.objtype == 'data' and getattr(
                    module,
                    type(obj).__name__, None) is not None:
                doc += '\n\n'.join([
                    super(UrlHelp, self).get_doc(descriptor),
                    "Class docstring\n===============",
                    '.. autoclass:: ' + type(obj).__name__
                ])
            # an instance --> use super get_doc for instance and the type
            elif descriptor.objtype == 'data':
                cls_doc = super(UrlHelp, self).get_doc(
                    self.describe_object(type(obj),
                                         type(obj).__name__))
                doc += '\n\n'.join([
                    super(UrlHelp, self).get_doc(descriptor),
                    "Class docstring\n===============", cls_doc
                ])
            # a function or method --> use super get_doc
            else:
                doc += super(UrlHelp, self).get_doc(descriptor)
        # otherwise the object has been defined in this session
        else:
            # an instance --> use super get_doc for instance and the type
            if descriptor.objtype == 'data':
                cls_doc = super(UrlHelp, self).get_doc(
                    self.describe_object(type(obj),
                                         type(obj).__name__))
                doc = '\n\n'.join([
                    super(UrlHelp, self).get_doc(descriptor),
                    "Class docstring\n===============", cls_doc
                ])
            # a function or method --> use super get_doc
            else:
                doc = super(UrlHelp, self).get_doc(descriptor)
        return doc.rstrip() + '\n'

    def process_docstring(self, lines, descriptor):
        """Process the lines with the napoleon sphinx extension"""
        lines = list(chain(*(l.splitlines() for l in lines)))
        lines = NumpyDocstring(lines,
                               what=descriptor.objtype,
                               name=descriptor.name,
                               obj=descriptor.obj).lines()
        lines = GoogleDocstring(lines,
                                what=descriptor.objtype,
                                name=descriptor.name,
                                obj=descriptor.obj).lines()
        return indent(
            super(UrlHelp, self).process_docstring(lines, descriptor))

    def close(self, *args, **kwargs):
        if self.sphinx_thread is not None:
            try:
                del self.sphinx_thread.app
            except AttributeError:
                pass
            shutil.rmtree(self.build_dir, ignore_errors=True)
            if self._temp_dir:
                shutil.rmtree(self.sphinx_dir, ignore_errors=True)
            del self.sphinx_thread
        return super(UrlHelp, self).close(*args, **kwargs)
예제 #3
0
class StraditizerMenuActions(StraditizerControlBase):
    """An object to control the main functionality of a Straditizer

    This object is creates menu actions to load the straditizer"""

    #: The QActions to save the straditizer, straditizer image, etc.
    save_actions = []

    #: The QActions to save the exported DataFrames and data images
    data_actions = []

    #: The QActions to save the column names images
    text_actions = []

    #: The action to
    #: :meth:`~straditize.widgets.StraditizerWidgets.switch_to_straditizer_layout`
    window_layout_action = None

    #: The path to a directory from where to open a straditizer. Is set in the
    #: tutorial
    _dirname_to_use = None

    @property
    def all_actions(self):
        """:attr:`save_actions`, :attr:`data_actions` and :attr:`text_actions`
        """
        return chain(self.save_actions, self.data_actions, self.text_actions)

    def __init__(self, straditizer_widgets):
        self.init_straditizercontrol(straditizer_widgets)

    def setup_menu_actions(self, main):
        """Create the actions for the file menu

        Parameters
        ----------
        main: psyplot_gui.main.MainWindow
            The mainwindow whose menubar shall be adapted"""
        # load buttons
        self.open_menu = menu = QMenu('Open straditizer')
        main.open_project_menu.addMenu(self.open_menu)

        self.load_stradi_action = self._add_action(
            menu,
            'Project or image',
            self.open_straditizer,
            tooltip='Reload a digitization project or load a picture')

        self.load_clipboard_action = self._add_action(
            menu,
            'From clipboard',
            self.from_clipboard,
            tooltip='Load a picture from the clipboard')

        # save and export data buttons
        self.save_straditizer_action = self._add_action(
            main.save_project_menu,
            'Save straditizer',
            self.save_straditizer,
            tooltip='Save the digitization project')

        self.save_straditizer_as_action = self._add_action(
            main.save_project_as_menu,
            'Save straditizer as',
            self.save_straditizer_as,
            tooltip='Save the digitization project to a different file')

        self.export_data_menu = menu = QMenu('Straditizer data')
        main.export_project_menu.addMenu(menu)

        self.export_full_action = self._add_action(
            menu,
            'Full data',
            self.export_full,
            tooltip='Export the full digitized data')

        self.export_final_action = self._add_action(
            menu,
            'Samples',
            self.export_final,
            tooltip='Export the data at the sample locations')

        # close menu
        self.close_straditizer_action = self._add_action(
            main.close_project_menu,
            'Close straditizer',
            self.straditizer_widgets.close_straditizer,
            tooltip='Close the current straditize project')

        self.close_all_straditizer_action = self._add_action(
            main.close_project_menu,
            'Close all straditizers',
            self.straditizer_widgets.close_all_straditizers,
            tooltip='Close all open straditize projects')

        # export image buttons
        self.export_images_menu = menu = QMenu('Straditizer image(s)')
        menu.setToolTipsVisible(True)
        main.export_project_menu.addMenu(menu)

        self.export_full_image_action = self._add_action(
            menu,
            'Full image',
            self.save_full_image,
            tooltip='Save the full image to a file')

        self.export_data_image_action = self._add_action(
            menu,
            'Save data image',
            self.save_data_image,
            tooltip='Save the binary image that represents the data part')

        self.export_text_image_action = self._add_action(
            menu,
            'Save text image',
            self.save_text_image,
            tooltip='Save the image part with the rotated column descriptions')

        # import image buttons
        self.import_images_menu = menu = QMenu('Import straditizer image(s)')
        menu.setToolTipsVisible(True)

        self.import_full_image_action = self._add_action(
            menu,
            'Full image',
            self.import_full_image,
            tooltip='Import the diagram into the current project')

        self.import_data_image_action = self._add_action(
            menu,
            'Data image',
            self.import_data_image,
            tooltip='Import the data part image')

        self.import_binary_image_action = self._add_action(
            menu,
            'Binary data image',
            self.import_binary_image,
            tooltip='Import the binary image for the data part')

        self.import_text_image_action = self._add_action(
            menu,
            'Text image',
            self.import_text_image,
            tooltip='Import the image for the column names')

        self.window_layout_action = main.window_layouts_menu.addAction(
            'Straditizer layout',
            self.straditizer_widgets.switch_to_straditizer_layout)

        self.save_actions = [
            self.export_full_image_action, self.save_straditizer_action,
            self.import_full_image_action
        ]
        self.data_actions = [
            self.export_data_image_action, self.export_full_action,
            self.export_final_action, self.import_binary_image_action,
            self.import_data_image_action
        ]
        self.text_actions = [
            self.export_text_image_action, self.import_text_image_action
        ]

        self.widgets2disable = [
            self.load_stradi_action, self.load_clipboard_action
        ]

        self.refresh()

    def setup_shortcuts(self, main):
        """Setup the shortcuts when switched to the straditizer layout

        Parameters
        ----------
        main: psyplot_gui.main.MainWindow
            The psyplot mainwindow"""
        main.register_shortcut(self.save_straditizer_action, QKeySequence.Save)
        main.register_shortcut(self.save_straditizer_as_action,
                               QKeySequence.SaveAs)
        main.register_shortcut(self.close_straditizer_action,
                               QKeySequence.Close)
        main.register_shortcut(
            self.close_all_straditizer_action,
            QKeySequence('Ctrl+Shift+W', QKeySequence.NativeText))

        main.register_shortcut(self.export_final_action,
                               QKeySequence('Ctrl+E', QKeySequence.NativeText))
        main.register_shortcut(
            self.export_full_action,
            QKeySequence('Ctrl+Shift+E', QKeySequence.NativeText))
        main.register_shortcut(self.load_stradi_action,
                               [QKeySequence.Open, QKeySequence.New])

    def _add_action(self, menu, *args, **kwargs):
        tooltip = kwargs.pop('tooltip', None)
        a = menu.addAction(*args, **kwargs)
        if tooltip:
            a.setToolTip(tooltip)
        return a

    @property
    def _start_directory(self):
        def check_current():
            dirname = osp.dirname(current)
            if osp.exists(dirname) and osp.isdir(dirname):
                return dirname

        if self.straditizer is not None:
            current = None
            for attr in 'project_file', 'image_file':
                try:
                    current = self.straditizer.get_attr(attr)
                except KeyError:
                    pass
                else:
                    start = check_current()
                    if start is not None:
                        break
            if current:
                return osp.splitext(current)[0]
        return os.getcwd()

    @docstrings.get_sectionsf('StraditizerMenuActions._open_image')
    def _open_image(self, fname=None):
        """Open an image file

        Parameters
        ----------
        fname: :class:`str`, :class:`PIL.Image.Image` or ``None``
            The path of the image file or the :class:`PIL.Image.Image`. If
            None, a QFileDialog is opened to request the file from the user.

        Returns
        -------
        PIL.Image.Image
            The image file or None if the operation has been cancelled by the
            user"""
        if fname is None or (not isinstance(fname, six.string_types)
                             and np.ndim(fname) < 2):
            fname = QFileDialog.getOpenFileName(
                self.straditizer_widgets, 'Stratigraphic diagram',
                self._dirname_to_use or self._start_directory, 'All images '
                '(*.jpeg *.jpg *.pdf *.png *.raw *.rgba *.tif *.tiff);;'
                'Joint Photographic Experts Group (*.jpeg *.jpg);;'
                'Portable Document Format (*.pdf);;'
                'Portable Network Graphics (*.png);;'
                'Tagged Image File Format(*.tif *.tiff);;'
                'All files (*)')
            if with_qt5:  # the filter is passed as well
                fname = fname[0]
        if not np.ndim(fname) and not fname:
            return
        elif np.ndim(fname) >= 2:
            return fname
        else:
            from PIL import Image
            with Image.open(fname) as _image:
                return Image.fromarray(np.array(_image.convert('RGBA')))

    @docstrings.with_indent(8)
    def import_full_image(self, fname=None):
        """Import the straditizer image from an external file

        This method imports the
        :attr:`straditize.straditizer.Straditizer.image` from an external
        file and sets it to the current straditizer.

        Parameters
        ----------
        %(StraditizerMenuActions._open_image.parameters)s"""
        image = self._open_image(fname)
        if image is not None:
            if self.straditizer is None:
                self.open_straditizer(image)
            else:
                self.straditizer.reset_image(image)

    @docstrings.with_indent(8)
    def import_data_image(self, fname=None):
        """Import the data reader image from an external file

        This method imports the
        :attr:`straditize.binary.DataReader.image` from an external
        file and sets it to the current
        :attr:`~straditize.straditizer.Straditizer.data_reader`.

        Parameters
        ----------
        %(StraditizerMenuActions._open_image.parameters)s

        See Also
        --------
        import_binary_image: To import the binary file"""
        image = self._open_image(fname)
        if image is not None:
            self.straditizer.data_reader.reset_image(image)

    @docstrings.with_indent(8)
    def import_binary_image(self, fname=None):
        """Import the binary data reader image from an external file

        This method imports the
        :attr:`straditize.binary.DataReader.binary` from an external
        file and sets it to the current
        :attr:`~straditize.straditizer.Straditizer.data_reader`.

        Parameters
        ----------
        %(StraditizerMenuActions._open_image.parameters)s"""
        image = self._open_image(fname)
        if image is not None:
            self.straditizer.data_reader.reset_image(image, binary=True)

    @docstrings.with_indent(8)
    def import_text_image(self, fname):
        """Import the column names reader image from an external file

        This method imports the
        :attr:`straditize.colnames.ColNamesReader.highres_image` from an
        external file and sets it to the current
        :attr:`~straditize.straditizer.Straditizer.colnames_reader`.

        Parameters
        ----------
        %(StraditizerMenuActions._open_image.parameters)s"""
        image = self._open_image(fname)
        if image is not None:
            self.straditizer.colnames_reader.highres_image = image

    def open_straditizer(self, fname=None, *args, **kwargs):
        """Open a straditizer from an image or project file

        Parameters
        ----------
        fname: :class:`str`, :class:`PIL.Image.Image` or ``None``
            The path to the file to import. If None, a QFileDialog is opened
            and the user is asked for a file name. The action then depends on
            the ending of ``fname``:

            ``'.nc'`` or ``'.nc4'``
                we expect a netCDF file and open it with
                :func:`xarray.open_dataset` and load the straditizer with the
                :meth:`straditize.straditizer.Straditizer.from_dataset`
                constructor
            ``'.pkl'``
                We expect a pickle file and load the straditizer with
                :func:`pickle.load`
            any other ending
                We expect an image file and use the :func:`PIL.Image.open`
                function

            At the end, the loading is finished with the :meth:`finish_loading`
            method"""
        from straditize.straditizer import Straditizer
        if fname is None or (not isinstance(fname, six.string_types)
                             and np.ndim(fname) < 2):
            fname = QFileDialog.getOpenFileName(
                self.straditizer_widgets, 'Straditizer project',
                self._dirname_to_use or self._start_directory,
                'Projects and images '
                '(*.nc *.nc4 *.pkl *.jpeg *.jpg *.pdf *.png *.raw *.rgba *.tif'
                ' *.tiff);;'
                'NetCDF files (*.nc *.nc4);;'
                'Pickle files (*.pkl);;'
                'All images '
                '(*.jpeg *.jpg *.pdf *.png *.raw *.rgba *.tif *.tiff);;'
                'Joint Photographic Experts Group (*.jpeg *.jpg);;'
                'Portable Document Format (*.pdf);;'
                'Portable Network Graphics (*.png);;'
                'Raw RGBA bitmap (*.raw *.rbga);;'
                'Tagged Image File Format(*.tif *.tiff);;'
                'All files (*)')
            if with_qt5:  # the filter is passed as well
                fname = fname[0]
        if not np.ndim(fname) and not fname:
            return
        elif np.ndim(fname) >= 2:
            stradi = Straditizer(fname, *args, **kwargs)
        elif fname.endswith('.nc') or fname.endswith('.nc4'):
            import xarray as xr
            ds = xr.open_dataset(fname)
            stradi = Straditizer.from_dataset(ds.load(), *args, **kwargs)
            stradi.set_attr('project_file', fname)
            ds.close()
            stradi.set_attr('loaded', str(dt.datetime.now()))
        elif fname.endswith('.pkl'):
            stradi = Straditizer.load(fname, *args, **kwargs)
            stradi.set_attr('project_file', fname)
            stradi.set_attr('loaded', str(dt.datetime.now()))
        else:
            from PIL import Image
            with Image.open(fname) as _image:
                image = Image.fromarray(np.array(_image.convert('RGBA')),
                                        'RGBA')
            w, h = image.size
            im_size = w * h
            if im_size > 20e6:
                recom_frac = 17403188.0 / im_size
                answer = (
                    QMessageBox.Yes if self.straditizer_widgets.always_yes else
                    QMessageBox.question(
                        self.straditizer_widgets, "Large straditizer image",
                        "This is a rather large image with %1.0f pixels. "
                        "Shall I reduce it to %1.0f%% of it's size for a "
                        "better interactive experience?<br>"
                        "If not, you can rescale it via<br><br>"
                        "Transform source image &rarr; Rescale image" %
                        (im_size, 100. * recom_frac)))
                if answer == QMessageBox.Yes:
                    image = image.resize((int(round(w * recom_frac)),
                                          int(round(h * recom_frac))))

            stradi = Straditizer(image, *args, **kwargs)
            stradi.set_attr('image_file', fname)
        self.finish_loading(stradi)
        self._dirname_to_use = None

    def finish_loading(self, stradi):
        """Finish the opening of a straditizer

        This method sets the
        :attr:`straditizer.widgets.StraditizerWidgets.straditizer`, shows the
        straditizer image, creates the navigation sliders and sets the
        straditizer in the console

        Parameters
        ----------
        stradi: straditize.straditizer.Straditizer
            The straditizer that just has been opened"""
        self.straditizer = stradi
        stradi.show_full_image()
        self.create_sliders(stradi)
        self.set_stradi_in_console()
        self.stack_zoom_window()
        self.straditizer_widgets.refresh()

    def create_sliders(self, stradi):
        """Create sliders to navigate in the given axes

        Parameters
        ----------
        stradi: straditize.straditizer.Straditizer
            The straditizer that just has been opened"""
        ax = stradi.ax
        try:
            manager = ax.figure.canvas.manager
            dock = manager.window
            fig_widget = manager.parent_widget
        except AttributeError:
            return
        from psyplot_gui.backend import FigureWidget
        import matplotlib.colors as mcol
        xs, ys = stradi.image.size
        fc = ax.figure.get_facecolor()
        rgb = tuple(np.round(np.array(mcol.to_rgb(fc)) * 255).astype(int))

        slh = QtWidgets.QSlider(Qt.Horizontal)
        slv = QtWidgets.QSlider(Qt.Vertical)

        slh.setStyleSheet("background-color:rgb{};".format(rgb))
        slv.setStyleSheet("background-color:rgb{};".format(rgb))

        slh.setMaximum(xs)
        slv.setMaximum(ys)
        slv.setInvertedAppearance(True)
        vbox = QVBoxLayout()
        hbox = QHBoxLayout()

        vbox.setSpacing(0)
        hbox.setSpacing(0)

        hbox.addWidget(fig_widget)
        hbox.addWidget(slv)
        vbox.addLayout(hbox)
        vbox.addWidget(slh)

        w = FigureWidget()
        w.dock = dock
        w.setLayout(vbox)
        dock.setWidget(w)

        ax.callbacks.connect('xlim_changed', self.update_x_navigation_sliders)
        ax.callbacks.connect('ylim_changed', self.update_y_navigation_sliders)
        self.update_x_navigation_sliders(ax)
        self.update_y_navigation_sliders(ax)
        ref = weakref.ref(ax)
        slh.valueChanged.connect(self.set_ax_xlim(ref))
        slv.valueChanged.connect(self.set_ax_ylim(ref))

    @staticmethod
    def set_ax_xlim(ax_ref):
        """Define a function to update xlim from a given centered value

        Parameters
        ----------
        ax_ref: matplotlib.axes.Axes
            The axes whose x-limits to update when the returned function is
            called

        Returns
        -------
        callable
            The function that can be called with a `val` to set the x-center
            of the `ax_ref`"""
        def update(val):
            ax = ax_ref()
            if ax in _updating or ax is None:
                return

            _updating.append(ax)
            lims = ax.get_xlim()
            diff = (lims[1] - lims[0]) / 2
            ax.set_xlim(val - diff, val + diff)
            ax.figure.canvas.draw_idle()
            _updating.remove(ax)

        return update

    @staticmethod
    def set_ax_ylim(ax_ref):
        """Define a function to update ylim from a given centered value

        Parameters
        ----------
        ax_ref: matplotlib.axes.Axes
            The axes whose y-limits to update when the returned function is
            called

        Returns
        -------
        callable
            The function that can be called with a `val` to set the y-center
            of the `ax_ref`"""
        def update(val):
            ax = ax_ref()
            if ax in _updating or ax is None:
                return

            _updating.append(ax)
            lims = ax.get_ylim()
            diff = (lims[1] - lims[0]) / 2
            ax.set_ylim(val - diff, val + diff)
            ax.figure.canvas.draw_idle()
            _updating.remove(ax)

        return update

    @staticmethod
    def update_x_navigation_sliders(ax):
        """Update the horizontal navigation slider for the given `ax``

        Set the horizontal slider of the straditizer figure depending on the
        x-limits of it's corresponding axes

        Parameters
        ----------
        ax: matplotlib.axes.Axes
            The :attr:`straditize.straditizer.Straditizer.ax` attribute of a
            straditizer
        """
        w = ax.figure.canvas.manager.window.widget()
        slh = w.layout().itemAt(1).widget()

        xc = np.mean(ax.get_xlim())

        slh.setValue(max(0, min(slh.maximum(), int(round(xc)))))

    @staticmethod
    def update_y_navigation_sliders(ax):
        """Update the vertical navigation slider for the given `ax``

        Set the vertical slider of the straditizer figure depending on the
        y-limits of it's corresponding axes

        Parameters
        ----------
        ax: matplotlib.axes.Axes
            The :attr:`straditize.straditizer.Straditizer.ax` attribute of a
            straditizer
        """
        w = ax.figure.canvas.manager.window.widget()
        slv = w.layout().itemAt(0).itemAt(1).widget()

        yc = np.mean(ax.get_ylim())
        slv.setValue(max(0, min(slv.maximum(), int(round(yc)))))

    def stack_zoom_window(self):
        """Stack the magnifier image above the help explorer"""
        from psyplot_gui.main import mainwindow
        if mainwindow.figures:
            found = False
            for stradi in self.straditizer_widgets._straditizers:
                if stradi is not self.straditizer and stradi.magni:
                    ref_dock = stradi.magni.ax.figure.canvas.manager.window
                    found = True
                    break
            if not found:
                ref_dock = mainwindow.help_explorer.dock
            dock = self.straditizer.magni.ax.figure.canvas.manager.window
            pos = mainwindow.dockWidgetArea(ref_dock)
            mainwindow.addDockWidget(pos, dock)
            if not found:
                mainwindow.addDockWidget(pos, ref_dock)
            else:
                mainwindow.tabifyDockWidget(ref_dock, dock)
                # show the zoom figure
                dock.widget().show_plugin()
                dock.raise_()
            mainwindow.figures.insert(-1, mainwindow.figures.pop(-1))
            # show the straditizer figure
            mainwindow.figures[-1].widget().show_plugin()
            mainwindow.figures[-1].raise_()

    def from_clipboard(self):
        """Open a straditizer from an Image in the clipboard

        This method uses the :func:`PIL.ImageGrab.grabclipboard` function to
        open a new straditizer from the clipboard."""
        from PIL import ImageGrab
        from straditize.straditizer import Straditizer
        image = ImageGrab.grabclipboard()
        if np.shape(image)[-1] == 3:
            image.putalpha(255)
        stradi = Straditizer(image)
        return self.finish_loading(stradi)

    def save_straditizer(self):
        """Save the straditizer to a file"""
        try:
            fname = self.straditizer.attrs.loc['project_file', 0]
        except KeyError:
            fname = None
        return self.save_straditizer_as(fname)

    def save_straditizer_as(self, fname=None):
        """Save the straditizer to a file

        Parameters
        ----------
        fname: str or ``None``
            If None, a QFileDialog is opened and the user has to provide a
            filename. The final action then depends on the ending of the
            chose file:

            ``'.pkl'``
                We save the straditizer with :meth:`pickle.dump`
            else
                We use the
                :meth:`straditize.straditizer.Straditizer.to_dataset` method
                and save the resulting dataset using the
                :meth:`xarray.Dataset.to_netcdf` method"""
        if fname is None or not isinstance(fname, six.string_types):
            fname = QFileDialog.getSaveFileName(
                self.straditizer_widgets, 'Straditizer file destination',
                self._start_directory,
                ('NetCDF files (*.nc *.nc4);;Pickle files (*.pkl);;'
                 'All files (*)'))
            if with_qt5:  # the filter is passed as well
                fname = fname[0]
        if not fname:
            return
        ending = os.path.splitext(fname)[1]
        self.straditizer.set_attr('saved', str(dt.datetime.now()))
        self.straditizer.set_attr('project_file', fname)
        if ending == '.pkl':
            self.straditizer.save(fname)
        else:
            ds = self.straditizer.to_dataset()
            # -- Compression with a level of 4. Requires netcdf4 engine
            comp = dict(zlib=True, complevel=4)
            encoding = {var: comp for var in ds.data_vars}

            ds.to_netcdf(fname, encoding=encoding, engine='netcdf4')

    @docstrings.get_sectionsf('StraditizerMenuActions._save_image')
    def _save_image(self, image, fname=None):
        """Save an image to a file

        Parameters
        ----------
        image: PIL.Image.Image
            The image to save
        fname: str or None
            The path of the target filename where to save the `image`. If None,
            A QFileDialog is opened and we ask the user for a filename"""
        if fname is None or not isinstance(fname, six.string_types):
            fname = QFileDialog.getSaveFileName(
                self.straditizer_widgets, 'Straditizer file destination',
                self._start_directory, 'All images '
                '(*.png *.jpeg *.jpg *.pdf *.tif *.tiff);;'
                'Joint Photographic Experts Group (*.jpeg *.jpg);;'
                'Portable Document Format (*.pdf);;'
                'Portable Network Graphics (*.png);;'
                'Tagged Image File Format(*.tif *.tiff);;'
                'All files (*)')
            if with_qt5:  # the filter is passed as well
                fname = fname[0]
        if not fname:
            return
        ext = osp.splitext(fname)[1]
        if ext.lower() in ['.jpg', '.jpeg', '.pdf'] and image.mode == 'RGBA':
            image = rgba2rgb(image)
        image.save(fname)

    docstrings.keep_params('StraditizerMenuActions._save_image.parameters',
                           'fname')

    @docstrings.with_indent(8)
    def save_text_image(self, fname=None):
        """Save the image of the colnames reader

        Save the :attr:`straditize.colnames.ColNamesReader.image` of the
        current :attr:`~straditize.straditizer.Straditizer.colnames_reader`

        Parameters
        ----------
        %(StraditizerMenuActions._save_image.parameters.fname)s
        """
        reader = self.straditizer.colnames_reader
        self._save_image(reader.highres_image, fname)

    def save_full_image(self, fname=None):
        """Save the image of the straditizer

        Save the :attr:`straditize.straditizer.Straditizer.image` of the
        current :attr:`~straditize.widgets.StraditizerWidgets.straditizer`

        Parameters
        ----------
        %(StraditizerMenuActions._save_image.parameters.fname)s
        """
        self._save_image(self.straditizer.image, fname)

    def save_data_image(self, fname=None):
        """Save the binary image of the data reader

        Save the :attr:`straditize.binary.DataReader.binary` of the
        current :attr:`~straditize.straditizer.Straditizer.data_reader`

        Parameters
        ----------
        %(StraditizerMenuActions._save_image.parameters.fname)s
        """
        arr = np.tile(self.straditizer.data_reader.binary[:, :, np.newaxis],
                      (1, 1, 4))
        arr[..., 3] *= 255
        arr[..., :3] = 0
        image = Image.fromarray(arr.astype(np.uint8), 'RGBA')
        self._save_image(image, fname)

    def set_stradi_in_console(self):
        """Set the straditizer in the console of the GUI mainwindow

        This sets the current
        :attr:`~straditize.widgets.StraditizerWidgets.straditizer` in psyplots
        :attr:`~psyplot_gui.main.MainWindow.console` as the ``stradi`` variable
        """
        from psyplot_gui.main import mainwindow
        global straditizer
        straditizer = self.straditizer
        if mainwindow is not None:
            mainwindow.console.run_command_in_shell(
                'from %s import straditizer as stradi' % __name__)
        straditizer = None

    @docstrings.get_sectionsf('StraditizerMenuActions._export_df')
    def _export_df(self, df, fname=None):
        """Export a data frame to a file

        This method opens an :class:`ExportDfDialog` to save a data frame

        Parameters
        ----------
        df: pandas.DataFrame
            The dataframe to save
        fname: str or None
            The path of the target filename where to save the `df`
            (see the :meth:`ExportDialog.export_df`)"""
        ExportDfDialog.export_df(self.straditizer_widgets, df,
                                 self.straditizer, fname)

    docstrings.keep_params('StraditizerMenuActions._export_df.parameters',
                           'fname')

    @docstrings.with_indent(8)
    def export_final(self, fname=None):
        """Export the final results

        This method exports the
        :attr:`straditize.straditizer.Straditizer.final_df` of the current
        :attr:`~straditize.widgets.StraditizerWidgets.straditizer`

        Parameters
        ----------
        %(StraditizerMenuActions._export_df.parameters.fname)s"""
        try:
            df = self.straditizer.final_df
        except Exception as e:
            self.straditizer_widgets.error_msg.showTraceback(
                e.message if six.PY2 else str(e))
        else:
            self._export_df(df, fname)

    @docstrings.with_indent(8)
    def export_full(self, fname=None):
        """Export the full digitized data

        This method exports the
        :attr:`straditize.straditizer.Straditizer.full_df` of the current
        :attr:`~straditize.widgets.StraditizerWidgets.straditizer`

        Parameters
        ----------
        %(StraditizerMenuActions._export_df.parameters.fname)s"""
        self._export_df(self.straditizer.full_df, fname)

    def refresh(self):
        stradi = self.straditizer
        import_stradi_action = getattr(self, 'import_full_image_action', None)
        if stradi is None:
            for w in filter(lambda w: w is not import_stradi_action,
                            self.all_actions):
                w.setEnabled(False)
        else:
            if stradi.data_reader is None:
                for w in self.data_actions:
                    w.setEnabled(False)
            else:
                reader = stradi.data_reader
                self.export_data_image_action.setEnabled(True)
                self.import_binary_image_action.setEnabled(True)
                self.import_data_image_action.setEnabled(True)
                self.export_full_action.setEnabled(reader.full_df is not None)
                self.export_final_action.setEnabled(reader.full_df is not None)
            for w in self.text_actions:
                w.setEnabled(stradi.colnames_reader is not None)
            for w in self.save_actions:
                w.setEnabled(True)

    def setup_children(self, item):
        tree = self.straditizer_widgets.tree

        # import menu
        import_child = QTreeWidgetItem(0)
        item.addChild(import_child)
        self.btn_import = QToolButton()
        self.btn_import.setText('Import images')
        self.btn_import.setMenu(self.import_images_menu)
        self.btn_import.setPopupMode(QToolButton.InstantPopup)
        tree.setItemWidget(import_child, 0, self.btn_import)

        # export menu
        export_child = QTreeWidgetItem(0)
        item.addChild(export_child)
        self.btn_export = QToolButton()
        self.btn_export.setText('Export images')
        self.btn_export.setMenu(self.export_images_menu)
        self.btn_export.setPopupMode(QToolButton.InstantPopup)
        tree.setItemWidget(export_child, 0, self.btn_export)
예제 #4
0
class RcParamsWidget(ConfigPage, QWidget):
    """A configuration page for RcParams instances

    This page displays the :class:`psyplot.config.rcsetup.RcParams` instance in
    the :attr:`rc` attribute and let's the user modify it.

    Notes
    -----
    After the initialization, you have to call the :meth:`initialize` method"""

    #: the rcParams to use (must be implemented by subclasses)
    rc = None

    #: the :class:`RcParamsTree` that is used to display the rcParams
    tree = None

    @property
    def propose_changes(self):
        """A signal that is emitted if the user changes the values in the
        rcParams"""
        return self.tree.propose_changes

    @property
    def validChanged(self):
        """A signal that is emitted if the user changes the valid state of this
        page"""
        return self.tree.validChanged

    @property
    def changed(self):
        """True if any changes are proposed by this config page"""
        return bool(next(self.tree.changed_rc(), None))

    @property
    def is_valid(self):
        """True if all the settings are valid"""
        return self.tree.is_valid

    @property
    def icon(self):
        """The icon of this instance in the :class:`Preferences` dialog"""
        return QIcon(get_icon('rcParams.png'))

    def __init__(self, *args, **kwargs):
        super(RcParamsWidget, self).__init__(*args, **kwargs)
        self.vbox = vbox = QVBoxLayout()

        self.description = QLabel(
            '<p>Modify the rcParams for your need. Changes will not be applied'
            ' until you click the Apply or Ok button.</p>'
            '<p>Values must be entered in yaml syntax</p>',
            parent=self)
        vbox.addWidget(self.description)
        self.tree = tree = RcParamsTree(self.rc,
                                        getattr(self.rc, 'validate', None),
                                        getattr(self.rc, 'descriptions', None),
                                        parent=self)
        tree.setSelectionMode(QAbstractItemView.MultiSelection)
        vbox.addWidget(self.tree)

        self.bt_select_all = QPushButton('Select All', self)
        self.bt_select_changed = QPushButton('Select changes', self)
        self.bt_select_none = QPushButton('Clear Selection', self)
        self.bt_export = QToolButton(self)
        self.bt_export.setText('Export Selection...')
        self.bt_export.setToolTip('Export the selected rcParams to a file')
        self.bt_export.setPopupMode(QToolButton.InstantPopup)
        self.export_menu = export_menu = QMenu(self)
        export_menu.addAction(self.save_settings_action())
        export_menu.addAction(self.save_settings_action(True))
        self.bt_export.setMenu(export_menu)
        hbox = QHBoxLayout()
        hbox.addWidget(self.bt_select_all)
        hbox.addWidget(self.bt_select_changed)
        hbox.addWidget(self.bt_select_none)
        hbox.addStretch(1)
        hbox.addWidget(self.bt_export)
        vbox.addLayout(hbox)

        self.setLayout(vbox)

        self.bt_select_all.clicked.connect(self.tree.selectAll)
        self.bt_select_none.clicked.connect(self.tree.clearSelection)
        self.bt_select_changed.clicked.connect(self.tree.select_changes)

    def save_settings_action(self, update=False, target=None):
        """Create an action to save the selected settings in the :attr:`tree`

        Parameters
        ----------
        update: bool
            If True, it is expected that the file already exists and it will be
            updated. Otherwise, existing files will be overwritten
        """
        def func():
            if update:
                meth = QFileDialog.getOpenFileName
            else:
                meth = QFileDialog.getSaveFileName
            if target is None:
                fname = meth(
                    self,
                    'Select a file to %s' % ('update' if update else 'create'),
                    self.default_path, 'YAML files (*.yml);;'
                    'All files (*)')
                if with_qt5:  # the filter is passed as well
                    fname = fname[0]
            else:
                fname = target
            if not fname:
                return
            if update:
                rc = self.rc.__class__(defaultParams=self.rc.defaultParams)
                rc.load_from_file(fname)
                old_keys = list(rc)
                selected = dict(self.tree.selected_rc())
                new_keys = list(selected)
                rc.update(selected)
                rc.dump(fname,
                        include_keys=old_keys + new_keys,
                        exclude_keys=[])
            else:
                rc = self.rc.__class__(self.tree.selected_rc(),
                                       defaultParams=self.rc.defaultParams)
                rc.dump(fname, exclude_keys=[])

        action = QAction('Update...' if update else 'Overwrite...', self)
        action.triggered.connect(func)
        return action

    def initialize(self, rcParams=None, validators=None, descriptions=None):
        """Initialize the config page

        Parameters
        ----------
        rcParams: dict
            The rcParams to use. If None, the :attr:`rc` attribute of this
            instance is used
        validators: dict
            A mapping from the `rcParams` key to the corresponding validation
            function for the value. If None, the
            :attr:`~psyplot.config.rcsetup.RcParams.validate` attribute of the
            :attr:`rc` attribute is used
        descriptions: dict
            A mapping from the `rcParams` key to it's description. If None, the
            :attr:`~psyplot.config.rcsetup.RcParams.descriptions` attribute of
            the :attr:`rc` attribute is used"""
        if rcParams is not None:
            self.rc = rcParams
            self.tree.rc = rcParams
        if validators is not None:
            self.tree.validators = validators
        if descriptions is not None:
            self.tree.descriptions = descriptions
        self.tree.initialize()

    def apply_changes(self):
        """Apply the changes in the config page"""
        self.tree.apply_changes()
예제 #5
0
class UrlHelp(UrlBrowser, HelpMixin):
    """Class to convert rst docstrings to html and show browsers"""

    #: Object containing the necessary fields to describe an object given to
    #: the help widget. The descriptor is set up by the :meth:`describe_object`
    #: method and contains an additional objtype attribute
    object_descriptor = namedtuple(
        'ObjectDescriptor', ['obj', 'name', 'objtype'])

    can_document_object = with_sphinx
    can_show_rst = with_sphinx

    #: menu button with different urls
    bt_url_menus = None

    #:
    sphinx_thread = None

    def __init__(self, *args, **kwargs):
        self._temp_dir = 'sphinx_dir' not in kwargs
        self.sphinx_dir = kwargs.pop('sphinx_dir', mkdtemp(prefix='psyplot_'))
        self.build_dir = osp.join(self.sphinx_dir, '_build', 'html')
        super(UrlHelp, self).__init__(*args, **kwargs)

        self.error_msg = PyErrorMessage(self)
        if with_sphinx:
            self.sphinx_thread = SphinxThread(self.sphinx_dir)
            self.sphinx_thread.html_ready[str].connect(self.browse)
            self.sphinx_thread.html_error[str].connect(
                self.error_msg.showTraceback)
            self.sphinx_thread.html_error[str].connect(logger.debug)
            rcParams.connect('help_explorer.render_docs_parallel',
                             self.reset_sphinx)
            rcParams.connect('help_explorer.use_intersphinx',
                             self.reset_sphinx)
            rcParams.connect('help_explorer.online',
                             self.reset_sphinx)

        self.bt_connect_console = QToolButton(self)
        self.bt_connect_console.setCheckable(True)
        if rcParams['console.connect_to_help']:
            self.bt_connect_console.setIcon(QIcon(get_icon(
                'ipython_console.png')))
            self.bt_connect_console.click()
        else:
            self.bt_connect_console.setIcon(QIcon(get_icon(
                'ipython_console_t.png')))
        self.bt_connect_console.clicked.connect(self.toogle_connect_console)
        rcParams.connect('console.connect_to_help',
                         self.update_connect_console)
        self.toogle_connect_console()

        # menu button with different urls
        self.bt_url_menus = QToolButton(self)
        self.bt_url_menus.setIcon(QIcon(get_icon('docu_button.png')))
        self.bt_url_menus.setToolTip('Browse documentations')
        self.bt_url_menus.setPopupMode(QToolButton.InstantPopup)

        docu_menu = QMenu(self)
        for name, url in six.iteritems(self.doc_urls):
            def to_url(b, url=url):
                self.browse(url)
            action = QAction(name, self)
            action.triggered.connect(to_url)
            docu_menu.addAction(action)
        self.bt_url_menus.setMenu(docu_menu)

        self.button_box.addWidget(self.bt_connect_console)
        self.button_box.addWidget(self.bt_url_menus)
        # toogle the lock again to set the bt_url_menus enabled state
        self.toogle_url_lock()

    def update_connect_console(self, connect):
        if (connect and not self.bt_connect_console.isChecked() or
                not connect and self.bt_connect_console.isChecked()):
            self.bt_connect_console.click()

    def toogle_connect_console(self):
        """Disable (or enable) the loading of web pages in www"""
        bt = self.bt_connect_console
        connect = bt.isChecked()
        bt.setIcon(QIcon(get_icon(
            'ipython_console.png' if connect else 'ipython_console_t.png')))
        bt.setToolTip("%sonnect the console to the help explorer" % (
            "Don't c" if connect else "C"))
        if rcParams['console.connect_to_help'] is not connect:
            rcParams['console.connect_to_help'] = connect

    def reset_sphinx(self, value):
        """Method that is called if the configuration changes"""
        if with_sphinx and hasattr(self.sphinx_thread, 'app'):
            del self.sphinx_thread.app

    @docstrings.dedent
    def show_help(self, obj, oname='', files=None):
        """
        Render the rst docu for the given object with sphinx and show it

        Parameters
        ----------
        %(HelpMixin.show_help.parameters)s
        """
        if self.bt_lock.isChecked():
            return
        return super(UrlHelp, self).show_help(obj, oname=oname, files=files)

    @docstrings.dedent
    def show_intro(self, text=''):
        """
        Show the intro text in the explorer

        Parameters
        ----------
        %(HelpMixin.show_intro.parameters)s"""
        if self.sphinx_thread is not None:
            with open(self.sphinx_thread.index_file, 'a') as f:
                f.write('\n' + text.strip() + '\n\n' +
                        'Table of Contents\n'
                        '=================\n\n.. toctree::\n')
            self.sphinx_thread.render(None, None)

    def show_rst(self, text, oname='', descriptor=None, files=None):
        """Render restructured text with sphinx and show it

        Parameters
        ----------
        %(HelpMixin.show_rst.parameters)s"""
        if self.bt_lock.isChecked() or self.sphinx_thread is None:
            return False
        if not oname and descriptor:
            oname = descriptor.name
        for f in files or []:
            shutil.copyfile(f, osp.join(self.sphinx_dir, osp.basename(f)))
        self.sphinx_thread.render(text, oname)
        return True

    def describe_object(self, obj, oname=''):
        """Describe an object using additionaly the object type from the
        :meth:`get_objtype` method

        Returns
        -------
        instance of :attr:`object_descriptor`
            The descriptor of the object"""
        return self.object_descriptor(obj, oname, self.get_objtype(obj))

    def browse(self, url):
        """Reimplemented to add file paths to the url string"""
        url = asstring(url)
        html_file = osp.join(self.sphinx_dir, '_build', 'html', url + '.html')
        if osp.exists(html_file):
            url = file2html(html_file)
        super(UrlHelp, self).browse(url)

    def toogle_url_lock(self):
        """Disable (or enable) the loading of web pages in www"""
        super(UrlHelp, self).toogle_url_lock()
        # enable or disable documentation button
        bt = self.bt_url_lock
        offline = bt.isChecked()
        try:
            self.bt_url_menus.setEnabled(not offline)
        except AttributeError:  # not yet initialized
            pass

    def url_changed(self, url):
        """Reimplemented to remove file paths from the url string"""
        try:
            url = asstring(url.toString())
        except AttributeError:
            pass
        if url.startswith('file://'):
            fname = html2file(url)
            if osp.samefile(self.build_dir, osp.commonprefix([
                    fname, self.build_dir])):
                url = osp.splitext(osp.basename(fname))[0]
        super(UrlHelp, self).url_changed(url)

    def header(self, descriptor, sig):
        return '%(name)s\n%(bars)s\n\n.. py:%(type)s:: %(name)s%(sig)s\n' % {
            'name': descriptor.name, 'bars': '-' * len(descriptor.name),
            'type': descriptor.objtype, 'sig': sig}

    def get_objtype(self, obj):
        """Get the object type of the given object and determine wheter the
        object is considered a class, a module, a function, method or data

        Parameters
        ----------
        obj: object

        Returns
        -------
        str
            One out of {'class', 'module', 'function', 'method', 'data'}"""
        if inspect.isclass(obj):
            return 'class'
        if inspect.ismodule(obj):
            return 'module'
        if inspect.isfunction(obj) or isinstance(obj, type(all)):
            return 'function'
        if inspect.ismethod(obj) or isinstance(obj, type(str.upper)):
            return 'method'
        return 'data'

    def is_importable(self, modname):
        """Determine whether members of the given module can be documented with
        sphinx by using the :func:`sphinx.util.get_module_source` function

        Parameters
        ----------
        modname: str
            The __name__ attribute of the module to import

        Returns
        -------
        bool
            True if sphinx can import the module"""
        try:
            get_module_source(modname)
            return True
        except Exception:
            return False

    def get_doc(self, descriptor):
        """Reimplemented to (potentially) use the features from
        sphinx.ext.autodoc"""
        obj = descriptor.obj
        if inspect.ismodule(obj):
            module = obj
        else:
            module = inspect.getmodule(obj)
        if module is not None and (re.match('__.*__', module.__name__) or
                                   not self.is_importable(module.__name__)):
            module = None
        isclass = inspect.isclass(obj)
        # If the module is available, we try to use autodoc
        if module is not None:
            doc = '.. currentmodule:: ' + module.__name__ + '\n\n'
            # a module --> use automodule
            if inspect.ismodule(obj):
                doc += self.header(descriptor, '')
                doc += '.. automodule:: ' + obj.__name__
            # an importable class --> use autoclass
            elif isclass and getattr(module, obj.__name__, None) is not None:
                doc += self.header(descriptor, '')
                doc += '.. autoclass:: ' + obj.__name__
            # an instance and the class can be imported
            # --> use super get_doc and autoclass for the tyoe
            elif descriptor.objtype == 'data' and getattr(
                    module, type(obj).__name__, None) is not None:
                doc += '\n\n'.join([
                    super(UrlHelp, self).get_doc(descriptor),
                    "Class docstring\n===============",
                    '.. autoclass:: ' + type(obj).__name__])
            # an instance --> use super get_doc for instance and the type
            elif descriptor.objtype == 'data':
                cls_doc = super(UrlHelp, self).get_doc(self.describe_object(
                    type(obj), type(obj).__name__))
                doc += '\n\n'.join([
                    super(UrlHelp, self).get_doc(descriptor),
                    "Class docstring\n===============",
                    cls_doc])
            # a function or method --> use super get_doc
            else:
                doc += super(UrlHelp, self).get_doc(descriptor)
        # otherwise the object has been defined in this session
        else:
            # an instance --> use super get_doc for instance and the type
            if descriptor.objtype == 'data':
                cls_doc = super(UrlHelp, self).get_doc(self.describe_object(
                    type(obj), type(obj).__name__))
                doc = '\n\n'.join([
                    super(UrlHelp, self).get_doc(descriptor),
                    "Class docstring\n===============",
                    cls_doc])
            # a function or method --> use super get_doc
            else:
                doc = super(UrlHelp, self).get_doc(descriptor)
        return doc.rstrip() + '\n'

    def process_docstring(self, lines, descriptor):
        """Process the lines with the napoleon sphinx extension"""
        lines = list(chain(*(l.splitlines() for l in lines)))
        lines = NumpyDocstring(
            lines, what=descriptor.objtype, name=descriptor.name,
            obj=descriptor.obj).lines()
        lines = GoogleDocstring(
            lines, what=descriptor.objtype, name=descriptor.name,
            obj=descriptor.obj).lines()
        return indent(super(UrlHelp, self).process_docstring(
            lines, descriptor))

    def close(self, *args, **kwargs):
        if self.sphinx_thread is not None:
            try:
                del self.sphinx_thread.app
            except AttributeError:
                pass
            shutil.rmtree(self.build_dir, ignore_errors=True)
            if self._temp_dir:
                shutil.rmtree(self.sphinx_dir, ignore_errors=True)
            del self.sphinx_thread
        return super(UrlHelp, self).close(*args, **kwargs)
예제 #6
0
class UrlHelp(UrlBrowser, HelpMixin):
    """Class to convert rst docstrings to html and show browsers"""

    #: Object containing the necessary fields to describe an object given to
    #: the help widget. The descriptor is set up by the :meth:`describe_object`
    #: method and contains an additional objtype attribute
    object_descriptor = namedtuple(
        'ObjectDescriptor', ['obj', 'name', 'objtype'])

    can_document_object = with_sphinx
    can_show_rst = with_sphinx

    def __init__(self, *args, **kwargs):
        self.sphinx_dir = kwargs.pop('sphinx_dir', mkdtemp())
        self.build_dir = osp.join(self.sphinx_dir, '_build', 'html')
        super(UrlHelp, self).__init__(*args, **kwargs)

        self.error_msg = PyErrorMessage(self)
        if with_sphinx:
            self.sphinx_thread = SphinxThread(self.sphinx_dir)
            self.sphinx_thread.html_ready[str].connect(self.browse)
            self.sphinx_thread.html_error[str].connect(
                self.error_msg.showTraceback)
            self.sphinx_thread.html_error[str].connect(logger.debug)
        else:
            self.sphinx_thread = None

        #: menu button with different urls
        self.bt_url_menus = QToolButton(self)
        self.bt_url_menus.setIcon(QIcon(get_icon('docu_button.png')))
        self.bt_url_menus.setToolTip('Browse documentations')
        self.bt_url_menus.setPopupMode(QToolButton.InstantPopup)

        docu_menu = QMenu(self)
        for name, url in six.iteritems(self.doc_urls):
            def to_url(b, url=url):
                self.browse(url)
            action = QAction(name, self)
            action.triggered.connect(to_url)
            docu_menu.addAction(action)
        self.bt_url_menus.setMenu(docu_menu)

        self.button_box.addWidget(self.bt_url_menus)

    @docstrings.dedent
    def show_intro(self, text=''):
        """
        Show the intro text in the explorer

        Parameters
        ----------
        %(HelpMixin.show_intro.parameters)s"""
        if self.sphinx_thread is not None:
            with open(self.sphinx_thread.index_file, 'a') as f:
                f.write(text)
            self.sphinx_thread.render(None, None)

    def show_rst(self, text, oname='', descriptor=None):
        """Render restructured text with sphinx and show it

        Parameters
        ----------
        %(HelpMixin.show_rst.parameters)s"""
        if not oname and descriptor:
            oname = descriptor.name
        self.sphinx_thread.render(text, oname)

    def describe_object(self, obj, oname=''):
        """Describe an object using additionaly the object type from the
        :meth:`get_objtype` method

        Returns
        -------
        instance of :attr:`object_descriptor`
            The descriptor of the object"""
        return self.object_descriptor(obj, oname, self.get_objtype(obj))

    def browse(self, url):
        """Reimplemented to add file paths to the url string"""
        html_file = osp.join(self.sphinx_dir, '_build', 'html', url + '.html')
        if osp.exists(html_file):
            url = 'file://' + html_file
        super(UrlHelp, self).browse(url)

    def url_changed(self, url):
        """Reimplemented to remove file paths from the url string"""
        try:
            url = url.toString()
        except AttributeError:
            pass
        if url.startswith('file://'):
            fname = url[7:]
            if osp.samefile(self.build_dir, osp.commonprefix([
                    fname, self.build_dir])):
                url = osp.splitext(osp.basename(fname))[0]
        super(UrlHelp, self).url_changed(url)

    def header(self, descriptor, sig):
        return '%(name)s\n%(bars)s\n\n.. py:%(type)s:: %(name)s%(sig)s\n' % {
            'name': descriptor.name, 'bars': '-' * len(descriptor.name),
            'type': descriptor.objtype, 'sig': sig}

    def get_objtype(self, obj):
        """Get the object type of the given object and determine wheter the
        object is considered a class, a module, a function, method or data

        Parameters
        ----------
        obj: object

        Returns
        -------
        str
            One out of {'class', 'module', 'function', 'method', 'data'}"""
        if inspect.isclass(obj):
            return 'class'
        if inspect.ismodule(obj):
            return 'module'
        if inspect.isfunction(obj) or isinstance(obj, type(all)):
            return 'function'
        if inspect.ismethod(obj) or isinstance(obj, type(str.upper)):
            return 'method'
        return 'data'

    def is_importable(self, modname):
        """Determine whether members of the given module can be documented with
        sphinx by using the :func:`sphinx.util.get_module_source` function

        Parameters
        ----------
        modname: str
            The __name__ attribute of the module to import

        Returns
        -------
        bool
            True if sphinx can import the module"""
        try:
            get_module_source(modname)
            return True
        except:
            return False

    def get_doc(self, descriptor):
        """Reimplemented to (potentially) use the features from
        sphinx.ext.autodoc"""
        obj = descriptor.obj
        if inspect.ismodule(obj):
            module = obj
        else:
            module = inspect.getmodule(obj)
        if module is not None and (re.match('__.*__', module.__name__) or
                                   not self.is_importable(module.__name__)):
            module = None
        isclass = inspect.isclass(obj)
        # If the module is available, we try to use autodoc
        if module is not None:
            doc = '.. currentmodule:: ' + module.__name__ + '\n\n'
            # a module --> use automodule
            if inspect.ismodule(obj):
                doc += self.header(descriptor, '')
                doc += '.. automodule:: ' + obj.__name__
            # an importable class --> use autoclass
            elif isclass and getattr(module, obj.__name__, None) is not None:
                doc += self.header(descriptor, '')
                doc += '.. autoclass:: ' + obj.__name__
            # an instance and the class can be imported
            # --> use super get_doc and autoclass for the tyoe
            elif descriptor.objtype == 'data' and getattr(
                    module, type(obj).__name__, None) is not None:
                doc += '\n\n'.join([
                    super(UrlHelp, self).get_doc(descriptor),
                    "Class docstring\n===============",
                    '.. autoclass:: ' + type(obj).__name__])
            # an instance --> use super get_doc for instance and the type
            elif descriptor.objtype == 'data':
                cls_doc = super(UrlHelp, self).get_doc(self.describe_object(
                    type(obj), type(obj).__name__))
                doc += '\n\n'.join([
                    super(UrlHelp, self).get_doc(descriptor),
                    "Class docstring\n===============",
                    cls_doc])
            # a function or method --> use super get_doc
            else:
                doc += super(UrlHelp, self).get_doc(descriptor)
        # otherwise the object has been defined in this session
        else:
            # an instance --> use super get_doc for instance and the type
            if descriptor.objtype == 'data':
                cls_doc = super(UrlHelp, self).get_doc(self.describe_object(
                    type(obj), type(obj).__name__))
                doc = '\n\n'.join([
                    super(UrlHelp, self).get_doc(descriptor),
                    "Class docstring\n===============",
                    cls_doc])
            # a function or method --> use super get_doc
            else:
                doc = super(UrlHelp, self).get_doc(descriptor)
        return doc.rstrip() + '\n'

    def process_docstring(self, lines, descriptor):
        """Process the lines with the napoleon sphinx extension"""
        lines = list(chain(*(l.splitlines() for l in lines)))
        lines = NumpyDocstring(
            lines, what=descriptor.objtype, name=descriptor.name,
            obj=descriptor.obj).lines()
        lines = GoogleDocstring(
            lines, what=descriptor.objtype, name=descriptor.name,
            obj=descriptor.obj).lines()
        return indent(super(UrlHelp, self).process_docstring(
            lines, descriptor))