Skip to content

Commit

Permalink
Merge pull request #710 from Dessia-tech/testing
Browse files Browse the repository at this point in the history
Towards 0.18.0
  • Loading branch information
GhislainJ authored Aug 2, 2024
2 parents 798a61e + af39916 commit 2af636a
Show file tree
Hide file tree
Showing 21 changed files with 510 additions and 282 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.18.0

### Added

- Displays : Tree is now a display
- Files : Method from_file now sets Binary and StringFile filename attribute
- Schemas : Order entry based on signature order

### Changed

- MultiObject : Now compute object names for sample names
- Workflow : Remove workflow display from WorkflowRun
- Displays : CAD/ volmdlr_primitives backward compatibility has been


## 0.17.0

### Added
Expand Down
3 changes: 1 addition & 2 deletions code_pylint.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from pylint import __version__
from pylint.lint import Run

MIN_NOTE = 9.5
MIN_NOTE = 9.7

EFFECTIVE_DATE = date(2023, 1, 18)
WEEKLY_DECREASE = 0.03
Expand All @@ -31,7 +31,6 @@
"too-many-locals": 5, # Reduce by dropping vectored objects
"too-many-branches": 8, # Huge refactor needed. Will be reduced by schema refactor
"unused-argument": 3, # Some abstract functions have unused arguments (plot_data). Hence cannot decrease
"cyclic-import": 2, # Still work to do on Specific based DessiaObject
"too-many-arguments": 18, # Huge refactor needed
"too-few-public-methods": 4, # Abstract classes (Errors, Checks,...)
"too-many-return-statements": 7, # Huge refactor needed. Will be reduced by schema refactor
Expand Down
92 changes: 0 additions & 92 deletions dessia_common/breakdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
import collections.abc
import numpy as npy

from dessia_common import REF_MARKER, OLD_REF_MARKER
import dessia_common.serialization as dcs
import dessia_common.utils.types as dct


def attrmethod_getter(object_, attr_methods):
Expand All @@ -33,96 +31,6 @@ def attrmethod_getter(object_, attr_methods):
return object_


class ExtractionError(Exception):
""" Custom Exception for deep attributes Extraction process. """


def extract_segment_from_object(object_, segment: str):
""" Try all ways to get an attribute (segment) from an object that can of numerous types. """
if dct.is_sequence(object_):
try:
return object_[int(segment)]
except ValueError as err:
message_error = (f"Cannot extract segment {segment} from object {{str(object_)[:500]}}:"
f" segment is not a sequence index")
raise ExtractionError(message_error) from err

if isinstance(object_, dict):
if segment in object_:
return object_[segment]

if segment.isdigit():
intifyed_segment = int(segment)
if intifyed_segment in object_:
return object_[intifyed_segment]
if segment in object_:
return object_[segment]
raise ExtractionError(f'Cannot extract segment {segment} from object {str(object_)[:200]}')

# should be a tuple
if segment.startswith('(') and segment.endswith(')') and ',' in segment:
key = []
for subsegment in segment.strip('()').replace(' ', '').split(','):
if subsegment.isdigit():
subkey = int(subsegment)
else:
subkey = subsegment
key.append(subkey)
return object_[tuple(key)]
raise ExtractionError(f"Cannot extract segment {segment} from object {str(object_)[:500]}")

# Finally, it is a regular object
return getattr(object_, segment)


def get_in_object_from_path(object_, path, evaluate_pointers=True):
""" Get deep attributes from an object. Argument 'path' represents path to deep attribute. """
segments = path.lstrip('#/').split('/')
element = object_
for segment in segments:
if isinstance(element, dict):
# Going down in the object and it is a reference : evaluating sub-reference
if evaluate_pointers:
if REF_MARKER in element:
try:
element = get_in_object_from_path(object_, element[REF_MARKER])
except RecursionError as err:
err_msg = f'Cannot get segment {segment} from path {path} in element {str(element)[:500]}'
raise RecursionError(err_msg) from err
elif OLD_REF_MARKER in element: # Retro-compatibility to be remove sometime
try:
element = get_in_object_from_path(object_, element[OLD_REF_MARKER])
except RecursionError as err:
err_msg = f'Cannot get segment {segment} from path {path} in element {str(element)[:500]}'
raise RecursionError(err_msg) from err

try:
element = extract_segment_from_object(element, segment)
except ExtractionError as err:

err_msg = f'Cannot get segment {segment} from path {path} in element {str(element)[:500]}'
raise ExtractionError(err_msg) from err

return element


def set_in_object_from_path(object_, path, value, evaluate_pointers=True):
""" Set deep attribute from an object to the given value. Argument 'path' represents path to deep attribute. """
reduced_path = '/'.join(path.lstrip('#/').split('/')[:-1])
last_segment = path.split('/')[-1]
if reduced_path:
last_object = get_in_object_from_path(object_, reduced_path, evaluate_pointers=evaluate_pointers)
else:
last_object = object_

if dct.is_sequence(last_object):
last_object[int(last_segment)] = value
elif isinstance(last_object, dict):
last_object[last_segment] = value
else:
setattr(last_object, last_segment, value)


def merge_breakdown_dicts(dict1, dict2):
""" Merge strategy of breakdown dictionaries. """
dict3 = dict1.copy()
Expand Down
148 changes: 110 additions & 38 deletions dessia_common/core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" Module to handle serialization for engineering objects. """

import base64
import io
import time
import warnings
import operator
Expand All @@ -18,9 +19,12 @@
import traceback as tb

from importlib import import_module
import numpy as npy

from dessia_common.utils.diff import data_eq, diff, choose_hash
from dessia_common.utils.types import is_bson_valid
from dessia_common import FLOAT_TOLERANCE
from dessia_common.utils.diff import diff, choose_hash
from dessia_common.utils.helpers import full_classname, is_sequence
from dessia_common.utils.types import is_bson_valid, isinstance_base_types
from dessia_common.utils.copy import deepcopy_value
import dessia_common.schemas.core as dcs
from dessia_common.serialization import SerializableObject, deserialize_argument, serialize
Expand All @@ -29,7 +33,7 @@
from dessia_common import templates
import dessia_common.checks as dcc
from dessia_common.displays import DisplayObject, DisplaySetting
from dessia_common.breakdown import attrmethod_getter, get_in_object_from_path
from dessia_common.breakdown import attrmethod_getter
import dessia_common.utils.helpers as dch
import dessia_common.files as dcf
from dessia_common.document_generator import DocxWriter
Expand Down Expand Up @@ -150,7 +154,7 @@ def _data_diff(self, other_object):

def _get_from_path(self, path: str):
""" Get object's deep attribute from given path. """
return get_in_object_from_path(self, path)
return dch.get_in_object_from_path(self, path)

@classmethod
def raw_schema(cls):
Expand Down Expand Up @@ -396,34 +400,43 @@ def plot(self, reference_path: str = "#", **kwargs):
for data in self.plot_data(reference_path, **kwargs):
plot_data.plot_canvas(plot_data_object=data,
canvas_id='canvas',
width=1400, height=900,
debug_mode=False)
width=1400, height=900)
else:
msg = f"Class '{self.__class__.__name__}' does not implement a plot_data method to define what to plot"
raise NotImplementedError(msg)

def mpl_plot(self, **kwargs):
def mpl_plot(self, selector: str):
""" Plot with matplotlib using plot_data function. """
axs = []
if hasattr(self, 'plot_data'):
try:
plot_datas = self.plot_data(**kwargs)
except TypeError as error:
raise TypeError(f'{self.__class__.__name__}.{error}') from error
for data in plot_datas:
if hasattr(data, 'mpl_plot'):
ax = data.mpl_plot()
axs.append(ax)
else:
msg = f"Class '{self.__class__.__name__}' does not implement a plot_data method to define what to plot"
raise NotImplementedError(msg)
return axs
display_setting = self._display_settings_from_selector(selector)
if display_setting.type != "plot_data":
raise NotImplementedError(f"Selector '{selector}' depicts a display of type '{display_setting.type}'"
f" which cannot be used to plot with matplotlib."
f"\nPlease select a 'plot_data' display setting.")
display = attrmethod_getter(self, display_setting.method)(**display_setting.arguments)
if hasattr(display, 'mpl_plot'):
return display.mpl_plot()
raise NotImplementedError(f"plot_data display of type '{display.__class__.__name__}'"
f" does not implement a mpl_plot converter."
f"\nSelector used : '{selector}'.")

def picture(self, stream, selector: str):
""" Take a stream to generate picture. """
ax = self.mpl_plot(selector)
ax.set_axis_off()
ax.figure.savefig(stream, format="png")
stream.seek(0)

@classmethod
def display_settings(cls, **kwargs) -> List[DisplaySetting]:
""" Return a list of objects describing how to call object displays. """
settings = [DisplaySetting(selector="markdown", type_="markdown", method="to_markdown", load_by_default=True)]
settings.extend(cls._display_settings_from_decorators())
decorators_settings = cls._display_settings_from_decorators()
has_markdown = any([s.type == "markdown" for s in decorators_settings])
settings = decorators_settings
if not has_markdown:
default_md = DisplaySetting(selector="Markdown", type_="markdown", method="to_markdown",
load_by_default=True)
settings.insert(0, default_md)
settings.append(DisplaySetting(selector="Structure Tree", type_="tree", method=""))
return settings

@classmethod
Expand Down Expand Up @@ -452,13 +465,12 @@ def _display_from_selector(self, selector: str) -> DisplayObject:
display_setting = self._display_settings_from_selector(selector)
track = ""
try:
data = attrmethod_getter(self, display_setting.method)(**display_setting.arguments)
display = attrmethod_getter(self, display_setting.method)(**display_setting.arguments)
except Exception:
data = None
display = None
track = tb.format_exc()

if display_setting.serialize_data:
data = serialize(data)
data = serialize(display) if display_setting.serialize_data else display
reference_path = display_setting.reference_path # Trying this
return DisplayObject(type_=display_setting.type, data=data, reference_path=reference_path, traceback=track)

Expand Down Expand Up @@ -652,7 +664,7 @@ def to_vector(self):
""" Compute vector from object. """
vectored_objects = []
for feature in self.vector_features():
vectored_objects.append(get_in_object_from_path(self, feature.lower()))
vectored_objects.append(dch.get_in_object_from_path(self, feature.lower()))
return vectored_objects

@classmethod
Expand All @@ -666,14 +678,6 @@ def vector_features(cls):
class PhysicalObject(DessiaObject):
""" Represent an object with CAD capabilities. """

@classmethod
def display_settings(cls, **kwargs):
""" Returns a list of DisplaySettings objects describing how to call sub-displays. """
display_settings = super().display_settings()
display_settings.append(DisplaySetting(selector='cad', type_='babylon_data',
method='volmdlr_volume_model().babylon_data', serialize_data=True))
return display_settings

def volmdlr_primitives(self, **kwargs):
""" Return a list of volmdlr primitives to build up volume model. """
warnings.warn("This method is deprecated and will be removed in a future version. "
Expand Down Expand Up @@ -907,7 +911,7 @@ def _comparison_operator(self):
return self._REAL_OPERATORS[self.comparison_operator]

def _to_lambda(self):
return lambda x: (self._comparison_operator()(get_in_object_from_path(value, f'#/{self.attribute}'),
return lambda x: (self._comparison_operator()(dch.get_in_object_from_path(value, f'#/{self.attribute}'),
self.bound) for value in x)

def get_booleans_index(self, values: List[DessiaObject]):
Expand Down Expand Up @@ -1199,3 +1203,71 @@ def get_attribute_names(object_class):
for item in [float, int, bool, complex])]
attributes += [a for a in subclass_numeric_attributes if a not in dcs.RESERVED_ARGNAMES]
return attributes


def data_eq(value1, value2):
""" Returns if two values are equal on data equality. """
if is_sequence(value1) and is_sequence(value2):
return sequence_data_eq(value1, value2)

if isinstance(value1, npy.int64) or isinstance(value2, npy.int64):
return value1 == value2

if isinstance(value1, npy.float64) or isinstance(value2, npy.float64):
return math.isclose(value1, value2, abs_tol=FLOAT_TOLERANCE)

if not isinstance(value2, type(value1)) and not isinstance(value1, type(value2)):
return False

if isinstance_base_types(value1):
if isinstance(value1, float):
return math.isclose(value1, value2, abs_tol=FLOAT_TOLERANCE)
return value1 == value2

if isinstance(value1, dict):
return dict_data_eq(value1, value2)

if isinstance(value1, (dcf.BinaryFile, dcf.StringFile)):
return value1 == value2

if isinstance(value1, type):
return full_classname(value1) == full_classname(value2)

# Else: its an object
if full_classname(value1) != full_classname(value2):
return False

# Test if _data_eq is customized
if hasattr(value1, '_data_eq'):
custom_method = value1._data_eq.__code__ is not DessiaObject._data_eq.__code__
if custom_method:
return value1._data_eq(value2)

# Not custom, use generic implementation
eq_dict = value1._data_eq_dict()
if 'name' in eq_dict:
del eq_dict['name']

other_eq_dict = value2._data_eq_dict()
return dict_data_eq(eq_dict, other_eq_dict)


def dict_data_eq(dict1, dict2):
""" Returns True if two dictionaries are equal on data equality, False otherwise. """
for key, value in dict1.items():
if key not in dict2:
return False
if not data_eq(value, dict2[key]):
return False
return True


def sequence_data_eq(seq1, seq2):
""" Returns if two sequences are equal on data equality. """
if len(seq1) != len(seq2):
return False

for v1, v2 in zip(seq1, seq2):
if not data_eq(v1, v2):
return False
return True
17 changes: 17 additions & 0 deletions dessia_common/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ def get_decorated_methods(class_: Type, decorator_name: str):
return [getattr(class_, n) for n in method_names]


def picture_view(selector: str = None, load_by_default: bool = False):
"""
Decorator to plot data pictures.
:param str selector: A custom and unique name that identifies the display.
It is what is displayed on platform to select your view.
:param bool load_by_default: Whether the view should be displayed on platform by default or not.
"""
def decorator(function):
""" Decorator to plot data."""
set_decorated_function_metadata(function=function, type_="picture", selector=selector,
serialize_data=True, load_by_default=load_by_default)
return function
return decorator


def plot_data_view(selector: str = None, load_by_default: bool = False):
"""
Decorator to plot data.
Expand Down
Loading

0 comments on commit 2af636a

Please sign in to comment.