diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 16df3def..06be58a1 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -52,8 +52,9 @@ jobs: tox - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v4.0.1 with: + token: ${{ secrets.CODECOV_TOKEN_NEW }} name: Pytest coverage env_vars: OS,PYTHON,GITHUB_ACTIONS,GITHUB_ACTION,GITHUB_REF,GITHUB_REPOSITORY,GITHUB_HEAD_REF,GITHUB_RUN_ID,GITHUB_SHA,COVERAGE_FILE env: diff --git a/src/easyscience/Utils/io/star.py b/src/easyscience/Utils/io/star.py deleted file mode 100644 index 14015a2b..00000000 --- a/src/easyscience/Utils/io/star.py +++ /dev/null @@ -1,520 +0,0 @@ -# SPDX-FileCopyrightText: 2023 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2021-2023 Contributors to the EasyScience project int: - return len(f'{round(self.error, self.decimal_places)}'.split('.')[-1]) - - def __str__(self) -> str: - if isinstance(self.value, Number): - initial_str = '{:.' + str(self.decimal_places) + 'f}' - s = initial_str.format(round(self.value, self.decimal_places)) - if self.error is not None: - # x_exp = int(floor(log10(self.value))) - xe_exp = int(floor(log10(self.error))) - - # uncertainty - un_exp = xe_exp - self.decimal_places + 1 - un_int = round(self.error * 10 ** (-un_exp)) - - # nominal value - no_exp = un_exp - no_int = round(self.value * 10 ** (-no_exp)) - - # format - nom(unc) - fmt = '%%.%df' % max(0, -no_exp) - s = (fmt + '(%.0f)') % ( - no_int * 10**no_exp, - un_int * 10 ** max(0, un_exp), - ) - elif isinstance(self.value, str): - s = '{:s}'.format(self.value) - else: - s = '{:s}'.format(str(self.value)) - # THIS IS THE OLD CODE, KEPT FOR REFERENCE - # s = "{}" - # if isinstance(self.value, str): - # s = "{:s}".format(self.value) - # else: - # v_in = [round(self.value, self.decimal_places)] - # if self.error is not None: - # digits = self._get_error_digits() - # if digits > self.decimal_places: - # v_in = [round(self.value, digits)] - # this_err = int(self.error * 10 ** digits) - # v_in.append(this_err) - # digits = digits - (len(str(this_err)) - 1) - # s = "{" + f":0.0{digits}f" + "}({})" - # s = s.format(*v_in) - if self.fixed is not None and not self.fixed and self.error is None: - s += '()' - return self._format_field(s) - - def _format_field(self, v): - v = v.__str__().strip() - if len(v) > self.maxlen: - return ';\n' + textwrap.fill(v, self.maxlen) + '\n;' - # add quotes if necessary - if v == '': - return '""' - if (' ' in v or v[0] == '_') and not (v[0] == "'" and v[-1] == "'") and not (v[0] == '"' and v[-1] == '"'): - if "'" in v: - q = '"' - else: - q = "'" - v = q + v + q - return v - - @staticmethod - def _makeFakeItem(in_string: str) -> FakeItem: - in_string = in_string.strip() - if "'" in in_string: - in_string = in_string.replace("'", '') - fixed = None - error = None - tokens = in_string.split('(') - try: - value = float(tokens[0]) - except ValueError: - value = tokens[0] - return FakeItem(value, error, fixed) - if len(tokens) > 1: - fixed = False - if tokens[1][0] != ')': - error = (10 ** -(len(f'{tokens[0]}'.split('.')[1]) + len(tokens[1][:-1]) - 1)) * int(tokens[1][:-1]) - return FakeItem(value, error, fixed) - - @classmethod - def from_string(cls, in_string: str): - return cls(cls._makeFakeItem(in_string)) - - def to_fake_item(self): - return FakeItem(self.value, self.error, self.fixed) - - -class StarEntry(ItemHolder): - def __init__(self, item, entry_name: str = None, prefix='_'): - if entry_name is None: - entry_name = item.name - - if len(entry_name) > _MAX_LABEL_LEN: - raise AttributeError(f'Max label length is {int(_MAX_LABEL_LEN)}') - - self.name = entry_name - self.prefix = prefix - super(StarEntry, self).__init__(item) - - def __str__(self) -> str: - s = '{}{} '.format(self.prefix, self.name) + super(StarEntry, self).__str__() - return s - - @classmethod - def from_string(cls, input_str: str, name_conversion: str = None, prefix='_'): - name, value = input_str.split(' ') - name = name[len(prefix) :] - if name_conversion: - name = name_conversion - return cls(cls._makeFakeItem(value), name) - - def to_class(self, cls, name_conversion: str = None): - if name_conversion is None: - name_conversion = self.name - if hasattr(cls, 'from_pars'): - new_obj = cls.from_pars(**{name_conversion: self.value}) - if hasattr(new_obj, 'fixed'): - if self.fixed is not None: - new_obj.fixed = self.fixed - if hasattr(new_obj, 'error'): - if self.error is not None: - new_obj.error = self.error - return new_obj - - -class StarProcess: - @classmethod - def _process_string(cls, string): - # remove comments - string = re.sub(r'(\s|^)#.*$', '', string, flags=re.MULTILINE) - # remove empty lines - string = re.sub(r'^\s*\n', '', string, flags=re.MULTILINE) - # remove non_ascii - # string = remove_non_ascii(string) - # since line breaks in .cif files are mostly meaningless, - # break up into a stream of tokens to parse, rejoining multiline - # strings (between semicolons) - q = deque() - multiline = False - ml = [] - # this regex splits on spaces, except when in quotes. - # starting quotes must not be preceded by non-whitespace - # (these get eaten by the first expression) - # ending quotes must not be followed by non-whitespace - p = re.compile(r"""([^'"\s][\S]*)|'(.*?)'(?!\S)|"(.*?)"(?!\S)""") - for line in string.splitlines(): - if multiline: - if line.startswith(';'): - multiline = False - q.append(('', '', '', ' '.join(ml))) - ml = [] - line = line[1:].strip() - else: - ml.append(line) - continue - if line.startswith(';'): - multiline = True - ml.append(line[1:].strip()) - else: - for s in p.findall(line): - # s is tuple. location of the data in the tuple - # depends on whether it was quoted in the input - q.append(s) - return q - - @classmethod - def _loadBlock(cls, in_string: str, prefix='_'): - q = cls._process_string(in_string) - data = OrderedDict() - loops = [] - while q: - s = q.popleft() - # cif keys aren't in quotes, so show up in s[0] - if s[0] == '_eof': - break - if s[0].startswith(prefix): - try: - data[s[0]] = ''.join(q.popleft()) - except IndexError: - data[s[0]] = '' - elif s[0].startswith('loop_'): - columns = [] - items = [] - this_data = OrderedDict() - while q: - s = q[0] - if s[0].startswith('loop_') or not s[0].startswith(prefix): - break - columns.append(''.join(q.popleft())) - this_data[columns[-1]] = [] - while q: - s = q[0] - if s[0].startswith('loop_') or s[0].startswith(prefix): - break - items.append(''.join(q.popleft())) - n = len(items) // len(columns) - if len(items) % n != 0: - raise ValueError('Wrong dimensions in loop_ block') - # loops.append(columns) - for k, v in zip(columns * n, items): - this_data[k].append(v.strip()) - loops.append(this_data) - elif ''.join(s).strip() != '': - warnings.warn('Possible issue in cif file' ' at line: {}'.format(''.join(s).strip())) - return data, loops - - -class StarBase(StarProcess): - def __init__( - self, - core, - entry_names: List[str] = None, - exclude: list = None, - prefix: str = '_', - ): - if not hasattr(core, '__len__'): - core = [core] - self.data = core - if exclude is None: - exclude = [] - self.exclude = exclude - - if self.data: - if entry_names is None: - entry_names = [self.data[0]._kwargs[key].name for key in self.data[0]._kwargs.keys() if key not in exclude] - else: - entry_names = [] - - self.prefix = prefix - self.labels = entry_names - self.maxlen = _MAX_LEN - - -class StarCollection(StarProcess): - def __init__(self, *star_objects): - self.data = star_objects - - def __str__(self) -> str: - return '\n\n'.join([str(data) for data in self.data]) - - @classmethod - def from_string(cls, in_string, prefix='_'): - in_string = '\n'.join([item for item in in_string.split('\n') if item and item[0] != '#']) - - blocks = in_string.split('data_') - data_blocks = [] - for block in blocks: - if not block: - continue - data_block = {'header': None, 'loops': [], 'data': {}} - items = block.split('\n') - if len(items) == 0: - continue - data_block['header'] = StarHeader.from_string('data_' + items[0]) - data, loops = cls._loadBlock('\n'.join(items[1:])) - for loop in loops: - data_block['loops'].append(StarLoop.from_data(loop, prefix=prefix)) - for key in data.keys(): - entry = StarEntry.from_string('{} {}'.format(key, data[key])) - data_block['data'][entry.name] = entry - data_blocks.append(data_block) - if len(data_blocks) == 1: - data_blocks = data_blocks[0] - return data_blocks - - @classmethod - def from_file(cls, filename: str): - with open(filename, 'r') as reader: - in_string = reader.read() - if not in_string: - in_string = filename - return cls.from_string(in_string) - - -class StarSection(StarBase): - def __str__(self) -> str: - return self._section_to_string() - - def _section_to_string(self): - s = '' - keys = [key for key in self.data[0]._kwargs.keys() if key not in self.exclude] - for idx, key in enumerate(keys): - s += f'{StarEntry(self.data[0]._kwargs[key], self.labels[idx], prefix=self.prefix)}\n' - return s - - def to_class(self, cls, name_conversions=None, skip=[]): - if not hasattr(cls, 'from_pars'): - raise AttributeError - if name_conversions is None: - name_conversions = [[k1, k2] for k1, k2 in zip(self.labels, self.data[0]._kwargs.keys())] - new_object = cls.from_pars(**{k[0]: self.data[0]._kwargs[k[1]].raw_value for k in name_conversions}) - for key in name_conversions: - attr = getattr(new_object, key[0]) - if hasattr(self.data[0]._kwargs[key[1]], 'fixed'): - if self.data[0]._kwargs[key[1]].fixed is not None: - attr.fixed = self.data[0]._kwargs[key[1]].fixed - if self.data[0]._kwargs[key[1]].error is not None: - attr.error = self.data[0]._kwargs[key[1]].error - return new_object - - @classmethod - def from_string(cls, in_string: str, name_conversion: List[str] = None, prefix='_'): - items = in_string.split('\n') - data = [FakeCore()] - names = [] - for idx, item in enumerate(items): - if not item: - continue - this_name = None - if name_conversion is not None: - this_name = name_conversion[idx] - conv_item = StarEntry.from_string(item, name_conversion=this_name, prefix=prefix) - names.append(conv_item.name) - data[0]._kwargs[conv_item.name] = conv_item.to_fake_item() - return cls(data, entry_names=names, prefix=prefix) - - @classmethod - def from_StarEntries( - cls, - star_entries: List[StarEntry], - name_conversions: List[str] = None, - prefix='_', - ): - data = [FakeCore()] - names = [] - for idx, entry in enumerate(star_entries): - names.append(entry.name) - data[0]._kwargs[entry.name] = entry.to_fake_item() - if name_conversions is None: - name_conversions = names - return cls(data, entry_names=name_conversions, prefix=prefix) - - def to_StarEntries(self) -> List[StarEntry]: - keys = [key for key in self.data[0]._kwargs.keys() if key not in self.exclude] - return [StarEntry(self.data[0]._kwargs[key], self.labels[idx], prefix=self.prefix) for idx, key in enumerate(keys)] - - -class StarLoop(StarBase): - def __str__(self) -> str: - return self._loop_to_string() - - def _loop_to_string(self): - s = 'loop_' - if len(self.data) == 0: - return '' - keys = [key for key in self.data[0]._kwargs.keys() if key not in self.exclude] - for idx, kw in enumerate(keys): - label = kw - if not isinstance(self.data[0]._kwargs[kw], FakeItem): - label = self.data[0]._kwargs[kw].name - if self.labels[idx] is not None: - label = self.labels[idx] - s += '\n {}'.format(self.prefix) + label - for item in self.data: - line = '\n' - for kw in keys: - val = str(ItemHolder(item._kwargs[kw])) - if val.count(' ') == 0 and val.count("'") > 0: - val = val.strip("'") - if val[0] == ';': - s += line + '\n' + val - line = '\n' - elif len(line) + len(val) + 2 < self.maxlen: - line += ' ' + val - else: - s += line - line = '\n ' + val - s += line - return s - - @classmethod - def from_string(cls, in_string: str, name_conversion: List[str] = None, prefix='_'): - data, loops = cls._loadBlock(in_string, prefix=prefix) - if len(loops) > 1: - raise ValueError(f'String has more than one loop: {len(loops)}') - return cls.from_data(loops[0], name_conversion, prefix) - - @classmethod - def from_StarSections( - cls, - star_sections: List[StarSection], - name_conversion: List[str] = None, - prefix='_', - ): - this_data = [section.data[0] for section in star_sections] - all_names = star_sections[0].labels - if name_conversion is not None: - all_names = name_conversion - return cls(this_data, all_names, prefix=prefix) - - def to_StarSections(self) -> List[StarSection]: - return [StarSection(section, self.labels, prefix=self.prefix) for section in self.data] - - @classmethod - def from_data(cls, loop: dict, name_conversion: List[str] = None, prefix='_'): - all_names = [] - all_data = [] - keys = list(loop.keys()) - for idx2 in range(len(loop[keys[0]])): - fk = FakeCore() - for idx, key in enumerate(keys): - this_name = key - if this_name[0] == '_': - this_name = this_name[1:] - if name_conversion is not None: - this_name = name_conversion[idx] - if idx2 == 0: - all_names.append(this_name) - conv_item = StarEntry.from_string( - '{}{} {}'.format(prefix, this_name, loop[key][idx2]), - this_name, - prefix=prefix, - ) - fk._kwargs[conv_item.name] = conv_item.to_fake_item() - all_data.append(fk) - return cls(all_data, all_names, prefix=prefix) - - def to_class(self, cls_outer, cls_inner, name_conversions=None): - if not hasattr(cls_inner, 'from_pars'): - raise AttributeError - new_objects = [] - for idx in range(len(self.data)): - if name_conversions is None: - keys = [key for key in self.data[idx]._kwargs.keys() if key not in self.exclude] - name_conversions = [[k, k] for k in keys] - new_object = cls_inner.from_pars(**{k[0]: self.data[idx]._kwargs[k[1]].raw_value for k in name_conversions}) - for key in name_conversions: - attr = getattr(new_object, key[0]) - if hasattr(self.data[idx]._kwargs[key[1]], 'fixed'): - if self.data[idx]._kwargs[key[1]].fixed is not None: - attr.fixed = self.data[idx]._kwargs[key[1]].fixed - if self.data[idx]._kwargs[key[1]].error is not None: - attr.error = self.data[idx]._kwargs[key[1]].error - new_objects.append(new_object) - return cls_outer(cls_outer.__name__, *new_objects) - - def join(self, otherLoop: 'StarLoop', key: str) -> 'StarLoop': - if key not in self.labels or key not in otherLoop.labels: - raise AttributeError('Key must be common in both StarLoops') - if len(self.data) != len(otherLoop.data): - raise AttributeError('There must be the same number of entries in both StarLoops') - joint = StarLoop.from_string(str(self)) - for dataset in otherLoop.data: - lookup_value = dataset._kwargs['label'].raw_value - try: - lookup_idx = [d._kwargs['label'].raw_value for d in self.data].index(lookup_value) - except ValueError: - raise AttributeError('Both StarLoops must contain the joining same keys') - joint.data[lookup_idx]._kwargs.update(dataset._kwargs) - joint.labels.extend([k for k in otherLoop.labels if k != key]) - return joint - - -class StarHeader: - def __init__(self, name: str): - self.name = name - - def __str__(self) -> str: - return 'data_' + self.name - - @classmethod - def from_string(cls, in_string: str): - name = in_string.split('data_')[1] - return cls(name) diff --git a/tests/unit_tests/utils/io_tests/test_star.py b/tests/unit_tests/utils/io_tests/test_star.py deleted file mode 100644 index d2c941a7..00000000 --- a/tests/unit_tests/utils/io_tests/test_star.py +++ /dev/null @@ -1,97 +0,0 @@ -# SPDX-FileCopyrightText: 2022 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -# © 2022 Contributors to the EasyScience project - -__author__ = "github.com/wardsimon" -__version__ = "0.0.1" - -import pytest - -from easyscience.models.polynomial import Line -from easyscience.Objects.Groups import BaseCollection -from easyscience.Objects.Variable import Descriptor -from easyscience.Objects.Variable import Parameter -from easyscience.Utils.io.star import ItemHolder -from easyscience.Utils.io.star import StarLoop -from easyscience.Utils.io.star import StarSection -import gc - - -@pytest.mark.parametrize( - "value, error, precision, expected", - ( - (1.234560e05, 1.230000e02, 1, "123500(100)"), - (1.234567e01, 1.230000e-03, 2, "12.3457(12)"), - (1.234560e-01, 1.230000e-04, 3, "0.123456(123)"), - (1.234560e-03, 1.234500e-08, 4, "0.00123456000(1234)"), - (1.234560e-05, 1.234000e-07, 1, "0.0000123(1)"), - ), - ids=[ - "1.234560e+05 +- 1.230000e+02 @1", - "1.234567e+01 +- 1.230000e-03 @2", - "1.234560e-01 +- 1.230000e-04 @3", - "1.234560e-03 +- 1.234500e-08 @4", - "1.234560e-05 +- 1.234000e-07 @1", - ], -) -def test_ItemHolder_with_error(value, error, precision, expected): - p = Parameter("p", value, error=error) - s = ItemHolder(p, decimal_places=precision) - assert str(s) == expected - - -@pytest.mark.parametrize("fixed", (True, False), ids=["fixed", "not fixed"]) -@pytest.mark.parametrize( - "value, precision, expected", - ( - (1.234560e05, 1, "123456.0"), - (1.234567e01, 2, "12.35"), - (1.234560e-01, 3, "0.123"), - (1.234560e-03, 4, "0.0012"), - (1.234560e-05, 1, "0.0"), - (1.234560e-05, 5, "0.00001"), - ), - ids=[ - "1.234560e+05 @1", - "1.234567e+01 @2", - "1.234560e-01 @3", - "1.234560e-03 @4", - "1.234560e-05 @1", - "1.234560e-05 @5", - ], -) -def test_ItemHolder_fixed(fixed, value, precision, expected): - p = Parameter("p", value, fixed=fixed) - s = ItemHolder(p, decimal_places=precision) - if not p.fixed: - expected += "()" - assert str(s) == expected - - -@pytest.mark.parametrize("cls", [Descriptor]) -def test_ItemHolder_str(cls): - v = cls("v", "fooooooooo") - s = ItemHolder(v) - assert str(s) == "fooooooooo" - - -def test_StarSection(): - l = Line(2, 3) - s = StarSection(l) - expected = "_m 2.00000000()\n_c 3.00000000()\n" - assert str(s) == expected - - -def test_StarLoop(): - gc.collect() - l1 = Line(2, 3) - l2 = Line(4, 5) - - ps = BaseCollection("LineCollection", l1, l2) - s = StarLoop(ps) - - expected = ( - "loop_\n _m\n _c\n 2.00000000() 3.00000000()\n 4.00000000() 5.00000000()" - ) - - assert str(s) == expected