diff --git a/examples/shrink_elements.py b/examples/shrink_elements.py index 0db6f622d..bcfc7e59a 100644 --- a/examples/shrink_elements.py +++ b/examples/shrink_elements.py @@ -17,7 +17,7 @@ direct_toedges.show_options["as_arrows"] = True # not the most efficient way, but it is possible. - gus.show.show_vedo( + gus.show( ["v, Volumes", v], ["v.shrink()", v.shrink()], [ diff --git a/gustaf/edges.py b/gustaf/edges.py index f7ab539c8..2679b067e 100644 --- a/gustaf/edges.py +++ b/gustaf/edges.py @@ -42,7 +42,7 @@ class EdgesShowOption(helpers.options.ShowOption): _helps = "Edges" - def _initialize_vedo_showable(self): + def _initialize_showable(self): """ Initializes edges as either vedo.Lines or vedo.Arrows diff --git a/gustaf/faces.py b/gustaf/faces.py index 83ebbe9f0..40fc86b5f 100644 --- a/gustaf/faces.py +++ b/gustaf/faces.py @@ -39,7 +39,7 @@ class FacesShowOption(helpers.options.ShowOption): _helps = "Faces" - def _initialize_vedo_showable(self): + def _initialize_showable(self): """ Initializes Faces as vedo.Mesh diff --git a/gustaf/helpers/options.py b/gustaf/helpers/options.py index 048ccd75c..6dea0158b 100644 --- a/gustaf/helpers/options.py +++ b/gustaf/helpers/options.py @@ -4,8 +4,6 @@ """ from copy import deepcopy -from gustaf import settings - class Option: """ @@ -55,14 +53,14 @@ def __init__( raise TypeError("Invalid description type") if isinstance(allowed_types, (tuple, list, set)): - self.allowed_types = set(allowed_types) + self.allowed_types = tuple(allowed_types) else: raise TypeError("Invalid allowed_types type") - if default is None or default in self.allowed_types: + if default is None or type(default) in self.allowed_types: self.default = default else: - raise TypeError("Invalid default type") + raise TypeError(f"{type(default)} is invalid default type") def __repr__(self): specific = "\n".join( @@ -70,11 +68,11 @@ def __repr__(self): "", self.key, "=" * len(self.key), - "backend: " + self.backend, + f"backends: {self.backends}", "description:", - " " + str(self.description), + f" {self.description}", "allowed_types:", - " " + str(self.allowed_types), + f" {self.allowed_types}", super().__repr__(), "", ] @@ -82,6 +80,18 @@ def __repr__(self): return specific +class SetDefault: + """ + Default setter object. Can use as argument in `make_valid_options` + """ + + __slots__ = ("key", "default") + + def __init__(self, key, default): + self.key = key + self.default = default + + # predefine recurring options vedo_common_options = ( Option( @@ -186,14 +196,20 @@ def make_valid_options(*options): """ valid_options = {} for opt in options: - if not isinstance(opt, Option): + if isinstance(opt, Option): + # copy option object to avoid overwriting defaults + valid_options[opt.key] = deepcopy(opt) + elif isinstance(opt, SetDefault): + # overwrite default of existing option. + if opt.key not in valid_options: + raise KeyError("Given key is not in valid_option.") + + if type(opt.default) not in valid_options[opt.key].allowed_types: + raise TypeError("Invalid default type") + valid_options[opt.key].default = opt.default + else: raise TypeError("Please use `Option` to define options.") - if not valid_options.get(opt.backend, False): - valid_options[opt.backend] = {} - - valid_options[opt.backend][opt.key] = opt - return valid_options @@ -207,7 +223,7 @@ class ShowOption: specific common routines. ShowOption and ShowManager in a sense. """ - __slots__ = ("_helpee", "_options", "_backend") + __slots__ = ("_helpee", "_options") _valid_options = {} @@ -226,10 +242,6 @@ def __init__(self, helpee): f"Given helpee is {type(helpee)}." ) self._options = {} - self._backend = settings.VISUALIZATION_BACKEND - - # initialize backend specific option holder - self._options[self._backend] = {} def __repr__(self): """ @@ -244,7 +256,7 @@ def __repr__(self): description: str """ valid_and_current = [] - for vo in self._valid_options[self._backend].values(): + for vo in self._valid_option.values(): valid = str(vo) current = "" if vo.key in self.keys(): @@ -253,7 +265,6 @@ def __repr__(self): header = [ f"ShowOption for {self._helps}", - f"Selected Backend: {self._backend}", ] return "\n".join(header + valid_and_current) @@ -270,29 +281,17 @@ def __setitem__(self, key, value): ------- None """ - if key in self._valid_options[self._backend]: + if key in self._valid_options: # valid type check - if not isinstance( - value, self._valid_options[self._backend][key].allowed_types - ): + if not isinstance(value, self._valid_options[key].allowed_types): raise TypeError( f"{type(value)} is invalid value type for '{key}'. " f"Details for '{key}':\n" - f"{self._valid_options[self._backend][key]}" + f"{self._valid_options[key]}" ) # types are valid. let's add - self._options[self._backend][key] = value - - elif key.startswith("backend"): - # special keyword. - if not isinstance(value, str): - raise TypeError( - f"Invalid backend info ({value}). Must be a str" - ) - self._backend = value - if not self._options.get(self._backend, False): - self._options[self._backend] = {} + self._options[key] = value else: raise ValueError(f"{key} is an invalid option for {self._helps}.") @@ -310,19 +309,20 @@ def __getitem__(self, key): items: object or dict """ if isinstance(key, str): - return self._options[self._backend][key] + return self._options[key] elif hasattr(key, "__iter__"): items = {} for k in key: - if k in self._options[self._backend]: - items[k] = self._options[self._backend][k] + if k in self._options: + items[k] = self._options[k] return items else: raise TypeError(f"Invalid key type for {type(self)}") - def get(self, key, default): + def get(self, key, default=None): """ - Gets value from key and default. Similar to dict.get() + Gets value from key and default. Similar to dict.get(), + but this is always safe, as it will always return None Parameters ---------- @@ -333,7 +333,13 @@ def get(self, key, default): ------- values: object """ - return self._options[self._backend].get(key, default) + if default is not None: + return self._options.get(key, default) + + # overwrite default with valid option's + default = getattr(self._valid_options.get(key, None), "default", None) + + return self._options.get(key, default) def update(self, **kwargs): """ @@ -350,21 +356,19 @@ def update(self, **kwargs): for k, v in kwargs.items(): self.__setitem__(k, v) - def valid_keys(self, backend=None): + def valid_keys(self): """ - Returns valid keys. Can directly specify backend. If not, returns - valid_keys for currently selected backend. + Returns valid keys. Parameters ---------- - backend: str + None Returns ------- valid_keys: dict_keys """ - backend = self._backend if backend is None else backend - return self._valid_options[backend].keys() + return self._valid_options.keys() def keys(self): """ @@ -378,7 +382,7 @@ def keys(self): ------- keys: dict_keys """ - return self._options[self._backend].keys() + return self._options.keys() def values(self): """ @@ -392,7 +396,7 @@ def values(self): ------- keys: dict_values """ - return self._options[self._backend].values() + return self._options.values() def items(self): """ @@ -406,7 +410,7 @@ def items(self): ------- items: dict_items """ - return self._options[self._backend].items() + return self._options.items() def clear(self): """ @@ -421,12 +425,10 @@ def clear(self): None """ self._options.clear() - # put back default backend option dict - self._options[self._backend] = {} def pop(self, *args, **kwargs): """ - Calls pop() on current backend options + Calls pop() on current options Parameters ---------- @@ -436,7 +438,7 @@ def pop(self, *args, **kwargs): ------- value: object """ - self._options[self._backend].pop(*args, **kwargs) + self._options.pop(*args, **kwargs) def copy_valid_options(self, copy_to, keys=None): """ @@ -474,4 +476,4 @@ def _initialize_showable(self): ------- showable: object """ - return eval(f"self._initialize_{self._backend}_showable()") + raise NotImplementedError("Derived class must implement this method") diff --git a/gustaf/show.py b/gustaf/show.py index 8f400fa12..3c23c1eb0 100644 --- a/gustaf/show.py +++ b/gustaf/show.py @@ -6,7 +6,7 @@ import numpy as np -from gustaf import settings, utils +from gustaf import utils # @linux it raises error if vedo is imported inside the function. try: @@ -34,30 +34,7 @@ def __call__(self, *args, **kwargs): sys.modules[__name__].__class__ = _CallableShowDotPy -def show(*gus_obj, **kwargs): - """Shows using appropriate backend. - - Parameters - ----------- - *gus_obj: gustaf objects - - Returns - -------- - None - """ - vis_b = settings.VISUALIZATION_BACKEND - - if vis_b.startswith("vedo"): - return show_vedo(*gus_obj, **kwargs) - elif vis_b.startswith("trimesh"): # noqa: SIM114 - pass - elif vis_b.startswith("matplotlib"): - pass - else: - raise NotImplementedError - - -def show_vedo( +def show( *args, **kwargs, ): @@ -164,7 +141,7 @@ def cam_tuple_to_list(dict_cam): sl = [sl] # noqa: PLW2901 for _k, item in enumerate(sl): if hasattr(item, "showable"): - tmp_showable = item.showable(backend="vedo", **kwargs) + tmp_showable = item.showable(**kwargs) # splines return dict # - maybe it is time to do some typing.. if isinstance(tmp_showable, dict): @@ -209,7 +186,7 @@ def cam_tuple_to_list(dict_cam): return plt -def _vedo_showable(obj, as_dict=False, **kwargs): +def make_showable(obj, as_dict=False, **kwargs): """Generates a vedo obj based on `kind` attribute from given obj, as well as show_options. @@ -383,46 +360,6 @@ def _vedo_showable(obj, as_dict=False, **kwargs): return return_as_dict -def _trimesh_showable(_obj): - """""" - pass - - -def _matplotlib_showable(_obj): - """""" - pass - - -def make_showable(obj, backend=settings.VISUALIZATION_BACKEND, **kwargs): - """Since gustaf does not natively support visualization, one of the - following library is used to visualize gustaf (visualizable) objects: (1) - vedo -> Fast, offers a lot of features (2) trimesh -> Fast, compatible with - old OpenGL (3) matplotlib -> Slow, offers vector graphics. - - This determines showing types using `whatami`. - - Parameters - ----------- - obj: gustaf-objects - backend: str - (Optional) Default is `gustaf.settings.VISUALIZATION_BACKEND`. - Options are: "vedo" | "trimesh" | "matplotlib" - - Returns - -------- - showable_objs: list - List of showable objects. - """ - if backend.startswith("vedo"): - return _vedo_showable(obj, **kwargs) - elif backend.startswith("trimesh"): - return _trimesh_showable(obj, **kwargs) - elif backend.startswith("matplotlib"): - return _matplotlib_showable(obj, **kwargs) - else: - raise NotImplementedError - - # possibly relocate, is this actually used? # could not find any usage in this repo def interpolate_vedo_dictcam(cameras, resolutions, spline_degree=1): diff --git a/gustaf/vertices.py b/gustaf/vertices.py index 8fca670f2..d7376adb1 100644 --- a/gustaf/vertices.py +++ b/gustaf/vertices.py @@ -42,7 +42,7 @@ class VerticesShowOption(helpers.options.ShowOption): _helps = "Vertices" - def _initialize_vedo_showable(self): + def _initialize_showable(self): """ Initialize Vertices showable for vedo. diff --git a/gustaf/volumes.py b/gustaf/volumes.py index 4dc5ee44b..c04db2ea8 100644 --- a/gustaf/volumes.py +++ b/gustaf/volumes.py @@ -22,7 +22,7 @@ class VolumesShowOption(helpers.options.ShowOption): _helps = "Volumes" - def _initialize_vedo_showable(self): + def _initialize_showable(self): """ Initialize volumes as vedo.UGrid or visually equivalent vedo.Mesh @@ -68,7 +68,7 @@ def _initialize_vedo_showable(self): faces = self._helpee.to_faces(unique=True) self.copy_valid_options(faces.show_options) - return faces.show_options._initialize_vedo_showable() + return faces.show_options._initialize_showable() class Volumes(Faces): diff --git a/tests/test_helpers/test_options.py b/tests/test_helpers/test_options.py index 862ab062f..5a3d52f36 100644 --- a/tests/test_helpers/test_options.py +++ b/tests/test_helpers/test_options.py @@ -1,19 +1,85 @@ import pytest -import gustaf as gus +from gustaf.helpers import options + + +def backend(): + return "vedo" + + +def key(): + return "color" + + +def description(): + return "color in german is Farbe" + + +def types(): + return (str, tuple, list) + + +def default(): + return "green" + + +def option(): + return options.Option(backend(), key(), description(), types(), default()) -def sample_option(): - return gus.helpers.options.Option( - "vedo", "color", "color in german is Farbe", (str, tuple, list), "green" - ) def test_option_init(): - op = gus.helpers.options.Option("vedo", "a", "abc", (str, int), "efg") - assert "vedo" in op.backends - assert op.key == "a" - assert op.description == "abc" - assert op.allowed_types == set(str, int) - assert op.default == "efg" + op = option() + assert backend() in op.backends + assert op.key == key() + assert op.description == description() + assert op.allowed_types == types() + assert op.default == default() + def test_option_type_check(): - pass \ No newline at end of file + wrong_type_default = 1 + assert type(wrong_type_default) not in types() + + with pytest.raises(TypeError): + options.Option(1, key(), description(), types(), default()) + with pytest.raises(TypeError): + options.Option(backend(), 1, description(), types(), default()) + with pytest.raises(TypeError): + options.Option(backend(), key(), 1, types(), default()) + with pytest.raises(TypeError): + options.Option(backend(), key(), description(), 1, default()) + with pytest.raises(TypeError): + options.Option( + backend(), key(), description(), types(), wrong_type_default + ) + + +def test_make_valid_options(): + option0 = option() + option1 = options.Option( + "backend", "key", "description", (int, float), 100 + ) + valid_opt = options.make_valid_options( + option0, + option1, + ) + + # vo should make a deepcopy + assert id(valid_opt[option0.key]) != id(option0) + assert id(valid_opt[option1.key]) != id(option1) + + o1_default = -100 + valid_opt_default_overwrite = options.make_valid_options( + option0, option1, options.SetDefault(option1.key, o1_default) + ) + + assert valid_opt_default_overwrite[option1.key].default == o1_default + # o0 default stays the same + assert valid_opt_default_overwrite[option0.key].default == default() + + with pytest.raises(TypeError): + valid_opt_default_overwrite = options.make_valid_options( + option0, + option1, + options.SetDefault(option1.key, "str is not allowed"), + )