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()
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)
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 → 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)
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()
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)
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))