Commit e7f5790c authored by schmittu's avatar schmittu 🍺
Browse files

merge back

parent e92a4c2a
......@@ -5,6 +5,18 @@ stages:
style:
stage: style
image: sissource.ethz.ch:5005/schmittu/emzed-gui/python-with-tox
tags:
- docker-executor
script:
- env | sort
- 'python -c "import sys; print(sys.platform)"'
- make style-check
only:
- master
windows_tests:
stage: test_code
tags:
- windows
script:
......
This diff is collapsed.
import emzed.lib as lib # noqa: F401
# This file is part of emzed (https://emzed.ethz.ch), a software toolbox for analysing
# LCMS data with Python.
#
# Copyright (C) 2020 ETH Zurich, SIS ID.
#
# This program is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this
# program. If not, see <http://www.gnu.org/licenses/>.
# Copyright (C) 2020 ETH Zurich, SIS ID.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import warnings
from inspect import currentframe
import pkg_resources
__version__ = pkg_resources.require(__package__.replace(".", "-"))[0].version
# profile decorator is only predefined when we run code with
# line_profiler (kernprof):
try:
profile
except NameError:
__builtins__["profile"] = lambda fun: fun
def silent_del(self):
pass
with warnings.catch_warnings():
# guiqwt creates logs of warnings
warnings.simplefilter("ignore")
from guiqwt.curve import CurvePlot
CurvePlot.__del__ = silent_del
from .dialog_builder import DialogBuilder
from .file_dialogs import (
ask_for_directory,
ask_for_multiple_files,
ask_for_save,
ask_for_single_file,
)
from .inspect import inspect
from .simple_dialogs import ask_yes_no, show_information, show_warning
def pyqt_enabled_set_trace():
from PyQt5.QtCore import pyqtRemoveInputHook
pyqtRemoveInputHook()
from pdb import set_trace
return set_trace(currentframe().f_back)
sys.breakpointhook = pyqt_enabled_set_trace
# This file is part of emzed (https://emzed.ethz.ch), a software toolbox for analysing
# LCMS data with Python.
#
# Copyright (C) 2020 ETH Zurich, SIS ID.
#
# This program is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this
# program. If not, see <http://www.gnu.org/licenses/>.
# Copyright (C) 2020 ETH Zurich, SIS ID.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import matplotlib.pyplot as plt
def getColors(i, light=False):
colors = [
(0, 0, 200),
(0, 150, 0),
(200, 0, 0),
(200, 200, 0),
(100, 70, 0),
]
colors = plt.get_cmap("tab10").colors
c = colors[i % len(colors)]
color = "#" + "".join("%02x" % round(255 * v) for v in c)
if light:
color = turn_light(color)
return color
def turn_light(color):
rgb = [int(color[i : i + 2], 16) for i in range(1, 6, 2)]
rgb_light = [min(ii + 50, 255) for ii in rgb]
return "#" + "".join("%02x" % v for v in rgb_light)
# This file is part of emzed (https://emzed.ethz.ch), a software toolbox for analysing
# LCMS data with Python.
#
# Copyright (C) 2020 ETH Zurich, SIS ID.
#
# This program is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this
# program. If not, see <http://www.gnu.org/licenses/>.
# Copyright (C) 2020 ETH Zurich, SIS ID.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from .colors import getColors
def configsForEics(n):
return [configForEic(i) for i in range(n)]
def configForEic(i):
return dict(linewidth=2, color=getColors(i))
def configs_for_fitted_peakshapes(smootheds):
n = len(smootheds)
return [
dict(shade=0.75, linewidth=3, color=getColors(i, light=True)) for i in range(n)
]
return [
dict(shade=0.35, linestyle="NoPen", color=getColors(i, light=True))
for i in range(n)
]
def configsForSpectra(n):
return [dict(color=getColors(i), linewidth=1) for i in range(n)]
# This file is part of emzed (https://emzed.ethz.ch), a software toolbox for analysing
# LCMS data with Python.
#
# Copyright (C) 2020 ETH Zurich, SIS ID.
#
# This program is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this
# program. If not, see <http://www.gnu.org/licenses/>.
# Copyright (C) 2020 ETH Zurich, SIS ID.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import locale
import os
import string
import sys
import types
import guidata
from guidata.utils import add_extension
import guidata.dataset.dataitems as di
import guidata.dataset.datatypes as dt
from guidata.dataset.qtwidgets import DataSetEditDialog
from guidata.qt.QtGui import QMessageBox
di_FilesOpenItem_from_string = di.FilesOpenItem.from_string
def from_string(self, value):
"""patched version of method. look at
guidata.dataset.dataitems.FilesOpenItem.from_string
to understand what we are doing here"""
# the original implementation has an eval() call which can
# fail!
try:
return di_FilesOpenItem_from_string(self, value)
except SyntaxError:
# Syntax Error only can be triggered when given value looks
# like a Python list of strings:
return [add_extension(value.strip("\"'[]"))]
di.FilesOpenItem.from_string = from_string
# monkey patch following Items, else dt.DataSet.check() raises
# exceptions. They are assumed to be valid in any case:
def _true(*a, **kw):
return True
if 0:
di.BoolItem.check_value = _true
di.ChoiceItem.check_value = _true
di.MultipleChoiceItem.check_value = _true
di.ButtonItem.check_value = _true
# patch ok / cancel: never check if needed fields are present ?!
# DataSetEditDialog.check = _true
def _patched_get(self, instance, klass):
if instance is not None:
value = getattr(instance, "_" + self._name, self._default)
if 0 and isinstance(value, unicode):
return value.encode(locale.getdefaultlocale()[1])
return value
return self
# di.StringItem.__get__ = _patched_get
# di.TextItem.__get__ = _patched_get
if sys.platform == "win32":
# replace needed for network pathes like "//gram/omics/...."
def _conv(s):
return s.encode(sys.getfilesystemencoding()).replace("/", "\\")
else:
def _conv(s):
return s
def _patched_get_for_pathes(self, instance, klass, _conv=_conv):
if instance is not None:
value = getattr(instance, "_" + self._name, self._default)
if isinstance(value, str):
value = _conv(value)
elif isinstance(value, (list, tuple)):
for i, item in enumerate(value):
if isinstance(item, str):
value[i] = _conv(item)
return value
return self
di.FilesOpenItem.__get__ = _patched_get_for_pathes
di.FileSaveItem.__get__ = _patched_get_for_pathes
di.FileOpenItem.__get__ = _patched_get_for_pathes
di.DirectoryItem.__get__ = _patched_get_for_pathes
def _translate_label_to_field_name(label):
# translate label strings to python variable names
invalid = r"""^°!"\§$%&/()=?´``+*~#'-.:,;<>|@$"""
trtable = str.maketrans(invalid, " " * len(invalid))
field_name = (
label.lower()
.translate(trtable)
.replace(" ", " ")
.replace(" ", " ")
.replace(" ", "_")
)
try:
exec("%s=0" % field_name) in dict()
except:
raise ValueError(
"converted label %r to field name %r "
"which is not allowed in python" % (label, field_name)
)
return field_name
class _Stub(object):
def __init__(self, item, to_wrap):
self.item = item
self.to_wrap = to_wrap
def __call__(self, label, *a, **kw):
# this function registers corresponding subclass of
# DataItem
if not isinstance(label, str):
raise ValueError("label must be a string")
if not label:
raise ValueError("you provided an empty label")
if label[0] not in string.ascii_letters:
raise ValueError("the first letter of the label must be a letter")
fieldName = _translate_label_to_field_name(label)
if self.item in (
di.FilesOpenItem,
di.FileOpenItem,
di.FileSaveItem,
di.DirectoryItem,
):
if "notempty" in kw:
kw["check"] = not kw["notempty"]
del kw["notempty"]
if "extensions" in kw:
kw["formats"] = kw["extensions"]
del kw["extensions"]
dd = dict((n, v) for (n, v) in kw.items() if n in ["col", "colspan"])
horizontal = kw.get("horizontal")
if horizontal is not None:
del kw["horizontal"]
vertical = kw.get("vertical")
if vertical is not None:
del kw["vertical"]
if "col" in kw:
del kw["col"]
if "colspan" in kw:
del kw["colspan"]
item = self.item(label, *a, **kw)
if dd:
item.set_pos(**dd)
if horizontal:
item.horizontal(horizontal)
if vertical:
item.vertical(vertical)
# regiter item and fieldname
self.to_wrap.items.append(item)
self.to_wrap.fieldNames.append(fieldName)
return self.to_wrap
class DialogBuilder(object):
# dynamic creation of __doc__
_docStrings = []
for _itemName, _item in di.__dict__.items():
if _itemName.endswith("Item"):
_docString = getattr(_item, "__doc__")
if _docString is None:
_docString = ""
_dynamicMethodName = " add" + _itemName[:-4]
_docStrings.append(_dynamicMethodName + "(...):\n" + _docString)
__doc__ = "\n".join(_docStrings)
def __init__(self, title="Dialog"):
self.attrnum = 0
self.title = title
self.items = []
self.instructions = []
self.fieldNames = []
self.buttonCounter = 0
def __getattr__(self, name):
"""dynamically provides methods which start with "add...", eg
"addInt(....)".
If one calls
b = Builder()
b.addInt(params)
then
b.addInt
is a stub function which is constructed and returned some
lines below. Then
b.addInt(params)
calls this stub function, which registers the corresponding
IntItem with the given params.
"""
if not name.startswith("add_"):
raise AttributeError("%r has no attribute '%s'" % (self, name))
guidata_name = (
"".join(part.capitalize() for part in name[4:].split("_")) + "Item"
)
if not hasattr(di, guidata_name):
raise AttributeError("%r has no attribute '%s'" % (self, name))
return self._get_stub(guidata_name, name)
def _get_stub(self, guidata_name, name):
item = getattr(di, guidata_name)
stub = _Stub(item, self)
# add docstring dynamically
# item = getattr(di, name[4:] + "Item")
docString = getattr(item, "__doc__")
docString = "" if docString is None else docString
docString = "-\n\n" + name + "(...):\n" + docString
stub.__doc__ = docString
return stub
def add_instruction(self, what):
self.instructions.append(what)
return self
def add_button(self, label, callback, help=None):
""" addButton is not handled by __getattr__, as it needs special
handling.
In contrast to the other DateItem subclasses, ButtonItem
gets a callback which has to be constructed in a special
way, see below.
"""
# the signature of 'wrapped' is dictated by the guidata
# framework:
def wrapped(ds, it, value, parent):
if not parent.check():
return
# check inputs before callback is executed
callback(ds)
# register ButtomItem in the same way other DataItem subclass
# instances are registered in the "stub" function in
# __getattr__:
item_instance = di.ButtonItem(label, wrapped, help=help)
self.items.append(item_instance)
self.fieldNames.append("_button%d" % self.buttonCounter)
self.buttonCounter += 1
return self
def show(self):
""" opens the constructed dialog.
In order to do so we construct sublcass of DataSet on the fly.
the docstring of the class is the title of the dialog,
class level attributes are instances of sublcasses of
DataItem, eg IntItem.
For more info see the docs of guidata how those classes
are declared to get the wanted dialog.
"""
import guidata
app = guidata.qapplication()
# put the class level attributes in a dict
attributes = dict(zip(self.fieldNames, self.items))
# construct class "Dialog" which is a sublcass of "dt.DataSet"
# with the given attributes:
clz = type("Dialog", (dt.DataSet,), attributes)
# as said: the docstring is rendered as the dialogues title:
clz.__doc__ = self.title + "\n" + "\n".join(self.instructions)
# open dialog now !!!
instance = clz()
if instance.edit() == 0:
raise Exception("dialog aborted by user")
# return the values a tuple according to the order of the
# declared input widgets:
result = [getattr(instance, name) for name in self.fieldNames]
result = tuple(result)
if len(result) == 1:
result = result[0]
return result
# import guidata DataItems into current namespace
for _itemName, _item in di.__dict__.items():
if _itemName.endswith("Item"):
exec("%s = di.%s" % (_itemName, _itemName))
def RunJobButton(label, method_name=None):
item = di.ButtonItem(label, None)
item._run_method = method_name
return item
class WorkflowFrontend(dt.DataSet):
def __init__(self):
import guidata
self.app = guidata.qapplication()
for item in self._items:
if hasattr(item, "_run_method"):
name = item._run_method or "run_" + item._name
target = getattr(self, name)
def inner(ds, it, value, parent, target=target):
ok = parent.check()
if not ok:
return
target()
setattr(self, "_emzed_run_" + name, inner)
item.set_prop("display", callback=inner)
dt.DataSet.__init__(self)
show = dt.DataSet.edit
# This file is part of emzed (https://emzed.ethz.ch), a software toolbox for analysing
# LCMS data with Python.
#
# Copyright (C) 2020 ETH Zurich, SIS ID.
#
# This program is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this
# program. If not, see <http://www.gnu.org/licenses/>.
# Copyright (C) 2020 ETH Zurich, SIS ID.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.