easyscience 2.0.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {easyscience-2.0.0 → easyscience-2.2.0}/.gitignore +3 -0
  2. {easyscience-2.0.0 → easyscience-2.2.0}/PKG-INFO +12 -2
  3. {easyscience-2.0.0 → easyscience-2.2.0}/README.md +6 -1
  4. {easyscience-2.0.0 → easyscience-2.2.0}/pyproject.toml +8 -1
  5. easyscience-2.2.0/src/easyscience/__version__.py +1 -0
  6. easyscience-2.2.0/src/easyscience/base_classes/__init__.py +8 -0
  7. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/base_classes/based_base.py +19 -4
  8. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/base_classes/collection_base.py +15 -11
  9. easyscience-2.2.0/src/easyscience/base_classes/easy_list.py +281 -0
  10. easyscience-2.2.0/src/easyscience/base_classes/model_base.py +119 -0
  11. easyscience-2.2.0/src/easyscience/base_classes/new_base.py +145 -0
  12. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/base_classes/obj_base.py +7 -15
  13. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/fitter.py +1 -0
  14. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/__init__.py +4 -1
  15. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_base.py +1 -1
  16. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_bumps.py +10 -3
  17. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_dfo.py +14 -7
  18. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/minimizer_lmfit.py +14 -4
  19. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/utils.py +1 -0
  20. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/__init__.py +1 -0
  21. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/hugger/hugger.py +18 -22
  22. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/hugger/property.py +66 -90
  23. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/logger.py +1 -3
  24. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/map.py +83 -36
  25. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/undo_redo.py +7 -1
  26. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/io/serializer_base.py +74 -1
  27. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/io/serializer_component.py +2 -3
  28. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/io/serializer_dict.py +3 -3
  29. easyscience-2.2.0/src/easyscience/job/__init__.py +8 -0
  30. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/job/analysis.py +6 -13
  31. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/job/experiment.py +2 -3
  32. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/job/job.py +9 -8
  33. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/job/theoreticalmodel.py +3 -3
  34. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/legacy/dict.py +9 -14
  35. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/models/__init__.py +3 -1
  36. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/models/polynomial.py +1 -2
  37. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/utils/__init__.py +1 -1
  38. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/utils/classUtils.py +1 -1
  39. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/utils/decorators.py +4 -6
  40. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/__init__.py +2 -0
  41. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_any_type.py +5 -6
  42. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_array.py +106 -102
  43. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_bool.py +1 -0
  44. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_number.py +23 -11
  45. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/parameter.py +280 -60
  46. easyscience-2.2.0/src/easyscience/variable/parameter_dependency_resolver.py +147 -0
  47. easyscience-2.0.0/src/easyscience/__version__.py +0 -1
  48. easyscience-2.0.0/src/easyscience/base_classes/__init__.py +0 -9
  49. easyscience-2.0.0/src/easyscience/job/__init__.py +0 -0
  50. {easyscience-2.0.0 → easyscience-2.2.0}/LICENSE +0 -0
  51. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/__init__.py +0 -0
  52. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/__init__.py +0 -0
  53. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/available_minimizers.py +0 -0
  54. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/calculators/__init__.py +0 -0
  55. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/calculators/interface_factory.py +0 -0
  56. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/minimizers/factory.py +0 -0
  57. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/fitting/multi_fitter.py +0 -0
  58. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/global_object.py +0 -0
  59. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/global_object/hugger/__init__.py +0 -0
  60. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/io/__init__.py +0 -0
  61. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/legacy/json.py +0 -0
  62. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/legacy/legacy_core.py +0 -0
  63. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/legacy/xml.py +0 -0
  64. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/utils/classTools.py +0 -0
  65. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/utils/string.py +0 -0
  66. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_base.py +1 -1
  67. {easyscience-2.0.0 → easyscience-2.2.0}/src/easyscience/variable/descriptor_str.py +0 -0
@@ -23,6 +23,9 @@ dist
23
23
  poetry.lock
24
24
  *.egg-info
25
25
 
26
+ # Pixi
27
+ .pixi/
28
+
26
29
  # PyInstaller
27
30
  build
28
31
  *.spec
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: easyscience
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: Generic logic for easyScience libraries
5
5
  Project-URL: homepage, https://github.com/EasyScience/EasyScience
6
6
  Project-URL: documentation, https://easyscience.github.io/EasyScience/
@@ -52,10 +52,15 @@ Requires-Dist: lmfit
52
52
  Requires-Dist: numpy
53
53
  Requires-Dist: scipp
54
54
  Requires-Dist: uncertainties
55
+ Provides-Extra: build
56
+ Requires-Dist: build; extra == 'build'
57
+ Requires-Dist: hatchling<=1.21.0; extra == 'build'
58
+ Requires-Dist: setuptools-git-versioning; extra == 'build'
55
59
  Provides-Extra: dev
56
60
  Requires-Dist: build; extra == 'dev'
57
61
  Requires-Dist: codecov; extra == 'dev'
58
62
  Requires-Dist: flake8; extra == 'dev'
63
+ Requires-Dist: jupyterlab; extra == 'dev'
59
64
  Requires-Dist: matplotlib; extra == 'dev'
60
65
  Requires-Dist: pytest; extra == 'dev'
61
66
  Requires-Dist: pytest-cov; extra == 'dev'
@@ -73,6 +78,7 @@ Description-Content-Type: text/markdown
73
78
  [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)
74
79
  [![PyPI badge](http://img.shields.io/pypi/v/EasyScience.svg)](https://pypi.python.org/pypi/EasyScience)
75
80
  [![License: BSD 3-Clause](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE)
81
+ [![codecov](https://codecov.io/github/EasyScience/corelib/graph/badge.svg?token=wc6Q0j0Q9t)](https://codecov.io/github/EasyScience/corelib)
76
82
 
77
83
  # Easyscience
78
84
 
@@ -91,6 +97,10 @@ Or direct from the repository:
91
97
 
92
98
  ```pip install https://github.com/easyScience/EasyScience```
93
99
 
100
+ ### Development
101
+
102
+ For development setup and workflow instructions, please see [CONTRIBUTING.md](CONTRIBUTING.md).
103
+
94
104
  ## Test
95
105
 
96
106
  After installation, launch the test suite:
@@ -101,7 +111,7 @@ After installation, launch the test suite:
101
111
 
102
112
  Documentation can be found at:
103
113
 
104
- [https://easyScience.github.io/EasyScience](https://easyScience.github.io/EasyScience)
114
+ [https://easyScience.github.io/corelib](https://easyScience.github.io/corelib)
105
115
 
106
116
  ## Contributing
107
117
  We absolutely welcome contributions. **EasyScience** is maintained by the ESS and on a volunteer basis and thus we need to foster a community that can support user questions and develop new features to make this software a useful tool for all users while encouraging every member of the community to share their ideas.
@@ -1,6 +1,7 @@
1
1
  [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)
2
2
  [![PyPI badge](http://img.shields.io/pypi/v/EasyScience.svg)](https://pypi.python.org/pypi/EasyScience)
3
3
  [![License: BSD 3-Clause](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](LICENSE)
4
+ [![codecov](https://codecov.io/github/EasyScience/corelib/graph/badge.svg?token=wc6Q0j0Q9t)](https://codecov.io/github/EasyScience/corelib)
4
5
 
5
6
  # Easyscience
6
7
 
@@ -19,6 +20,10 @@ Or direct from the repository:
19
20
 
20
21
  ```pip install https://github.com/easyScience/EasyScience```
21
22
 
23
+ ### Development
24
+
25
+ For development setup and workflow instructions, please see [CONTRIBUTING.md](CONTRIBUTING.md).
26
+
22
27
  ## Test
23
28
 
24
29
  After installation, launch the test suite:
@@ -29,7 +34,7 @@ After installation, launch the test suite:
29
34
 
30
35
  Documentation can be found at:
31
36
 
32
- [https://easyScience.github.io/EasyScience](https://easyScience.github.io/EasyScience)
37
+ [https://easyScience.github.io/corelib](https://easyScience.github.io/corelib)
33
38
 
34
39
  ## Contributing
35
40
  We absolutely welcome contributions. **EasyScience** is maintained by the ESS and on a volunteer basis and thus we need to foster a community that can support user questions and develop new features to make this software a useful tool for all users while encouraging every member of the community to share their ideas.
@@ -38,6 +38,11 @@ dependencies = [
38
38
  ]
39
39
 
40
40
  [project.optional-dependencies]
41
+ build = [
42
+ "hatchling <= 1.21.0",
43
+ "setuptools-git-versioning",
44
+ "build"
45
+ ]
41
46
  dev = [
42
47
  "build",
43
48
  "codecov",
@@ -46,7 +51,8 @@ dev = [
46
51
  "pytest",
47
52
  "pytest-cov",
48
53
  "ruff",
49
- "tox-gh-actions"
54
+ "tox-gh-actions",
55
+ "jupyterlab"
50
56
  ]
51
57
  docs = [
52
58
  "doc8",
@@ -136,6 +142,7 @@ python =
136
142
  PLATFORM =
137
143
  ubuntu-latest: linux
138
144
  macos-latest: macos
145
+ macos-15-intel: macos-intel
139
146
  windows-latest: windows
140
147
  [testenv]
141
148
  passenv =
@@ -0,0 +1 @@
1
+ __version__ = '2.2.0'
@@ -0,0 +1,8 @@
1
+ from .based_base import BasedBase
2
+ from .collection_base import CollectionBase
3
+ from .easy_list import EasyList
4
+ from .model_base import ModelBase
5
+ from .new_base import NewBase
6
+ from .obj_base import ObjBase
7
+
8
+ __all__ = [BasedBase, CollectionBase, ObjBase, ModelBase, NewBase, EasyList]
@@ -3,8 +3,10 @@ from __future__ import annotations
3
3
  # SPDX-FileCopyrightText: 2025 EasyScience contributors <core@easyscience.software>
4
4
  # SPDX-License-Identifier: BSD-3-Clause
5
5
  # © 2021-2025 Contributors to the EasyScience project <https://github.com/easyScience/EasyScience
6
- from inspect import getfullargspec
6
+ from inspect import signature
7
7
  from typing import TYPE_CHECKING
8
+ from typing import Any
9
+ from typing import Dict
8
10
  from typing import Iterable
9
11
  from typing import List
10
12
  from typing import Optional
@@ -38,9 +40,9 @@ class BasedBase(SerializerComponent):
38
40
  @property
39
41
  def _arg_spec(self) -> Set[str]:
40
42
  base_cls = getattr(self, '__old_class__', self.__class__)
41
- spec = getfullargspec(base_cls.__init__)
42
- names = set(spec.args[1:])
43
- return names
43
+ sign = signature(base_cls.__init__)
44
+ names = [param.name for param in sign.parameters.values() if param.kind == param.POSITIONAL_OR_KEYWORD]
45
+ return set(names[1:])
44
46
 
45
47
  def __reduce__(self):
46
48
  """
@@ -195,4 +197,17 @@ class BasedBase(SerializerComponent):
195
197
  new_obj = self.__class__.from_dict(temp)
196
198
  return new_obj
197
199
 
200
+ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]:
201
+ """
202
+ Convert an object into a full dictionary using `SerializerDict`.
203
+ This is a shortcut for ```obj.encode(encoder=SerializerDict)```
198
204
 
205
+ :param skip: List of field names as strings to skip when forming the dictionary
206
+ :return: encoded object containing all information to reform an EasyScience object.
207
+ """
208
+ # extend skip to include unique_name by default
209
+ if skip is None:
210
+ skip = []
211
+ if 'unique_name' not in skip:
212
+ skip.append('unique_name')
213
+ return super().as_dict(skip=skip)
@@ -14,6 +14,7 @@ from typing import Optional
14
14
  from typing import Tuple
15
15
  from typing import Union
16
16
 
17
+ from easyscience.base_classes.new_base import NewBase
17
18
  from easyscience.global_object.undo_redo import NotarizedDict
18
19
 
19
20
  from ..variable.descriptor_base import DescriptorBase
@@ -23,7 +24,6 @@ if TYPE_CHECKING:
23
24
  from ..fitting.calculators import InterfaceFactoryTemplate
24
25
 
25
26
 
26
-
27
27
  class CollectionBase(BasedBase, MutableSequence):
28
28
  """
29
29
  This is the base class for which all higher level classes are built off of.
@@ -35,7 +35,7 @@ class CollectionBase(BasedBase, MutableSequence):
35
35
  def __init__(
36
36
  self,
37
37
  name: str,
38
- *args: Union[BasedBase, DescriptorBase],
38
+ *args: Union[BasedBase, DescriptorBase, NewBase],
39
39
  interface: Optional[InterfaceFactoryTemplate] = None,
40
40
  unique_name: Optional[str] = None,
41
41
  **kwargs,
@@ -65,7 +65,7 @@ class CollectionBase(BasedBase, MutableSequence):
65
65
  _kwargs[key] = item
66
66
  kwargs = _kwargs
67
67
  for item in list(kwargs.values()) + _args:
68
- if not issubclass(type(item), (DescriptorBase, BasedBase)):
68
+ if not issubclass(type(item), (DescriptorBase, BasedBase, NewBase)):
69
69
  raise AttributeError('A collection can only be formed from easyscience objects.')
70
70
  args = _args
71
71
  _kwargs = {}
@@ -91,19 +91,19 @@ class CollectionBase(BasedBase, MutableSequence):
91
91
  self.interface = interface
92
92
  self._kwargs._stack_enabled = True
93
93
 
94
- def insert(self, index: int, value: Union[DescriptorBase, BasedBase]) -> None:
94
+ def insert(self, index: int, value: Union[DescriptorBase, BasedBase, NewBase]) -> None:
95
95
  """
96
96
  Insert an object into the collection at an index.
97
97
 
98
98
  :param index: Index for EasyScience object to be inserted.
99
99
  :type index: int
100
100
  :param value: Object to be inserted.
101
- :type value: Union[BasedBase, DescriptorBase]
101
+ :type value: Union[BasedBase, DescriptorBase, NewBase]
102
102
  :return: None
103
103
  :rtype: None
104
104
  """
105
105
  t_ = type(value)
106
- if issubclass(t_, (BasedBase, DescriptorBase)):
106
+ if issubclass(t_, (BasedBase, DescriptorBase, NewBase)):
107
107
  update_key = list(self._kwargs.keys())
108
108
  values = list(self._kwargs.values())
109
109
  # Update the internal dict
@@ -118,7 +118,7 @@ class CollectionBase(BasedBase, MutableSequence):
118
118
  else:
119
119
  raise AttributeError('Only EasyScience objects can be put into an EasyScience group')
120
120
 
121
- def __getitem__(self, idx: Union[int, slice]) -> Union[DescriptorBase, BasedBase]:
121
+ def __getitem__(self, idx: Union[int, slice]) -> Union[DescriptorBase, BasedBase, NewBase]:
122
122
  """
123
123
  Get an item in the collection based on its index.
124
124
 
@@ -152,7 +152,7 @@ class CollectionBase(BasedBase, MutableSequence):
152
152
  keys = list(self._kwargs.keys())
153
153
  return self._kwargs[keys[idx]]
154
154
 
155
- def __setitem__(self, key: int, value: Union[BasedBase, DescriptorBase]) -> None:
155
+ def __setitem__(self, key: int, value: Union[BasedBase, DescriptorBase, NewBase]) -> None:
156
156
  """
157
157
  Set an item via it's index.
158
158
 
@@ -164,7 +164,7 @@ class CollectionBase(BasedBase, MutableSequence):
164
164
  if isinstance(value, Number): # noqa: S3827
165
165
  item = self.__getitem__(key)
166
166
  item.value = value
167
- elif issubclass(type(value), (BasedBase, DescriptorBase)):
167
+ elif issubclass(type(value), (BasedBase, DescriptorBase, NewBase)):
168
168
  update_key = list(self._kwargs.keys())
169
169
  values = list(self._kwargs.values())
170
170
  old_item = values[key]
@@ -232,9 +232,13 @@ class CollectionBase(BasedBase, MutableSequence):
232
232
  return tuple(self._kwargs.values())
233
233
 
234
234
  def __repr__(self) -> str:
235
- return f"{self.__class__.__name__} `{getattr(self, 'name')}` of length {len(self)}"
235
+ return f'{self.__class__.__name__} `{getattr(self, "name")}` of length {len(self)}'
236
236
 
237
- def sort(self, mapping: Callable[[Union[BasedBase, DescriptorBase]], Any], reverse: bool = False) -> None:
237
+ def sort(
238
+ self,
239
+ mapping: Callable[[Union[BasedBase, DescriptorBase, NewBase]], Any],
240
+ reverse: bool = False,
241
+ ) -> None:
238
242
  """
239
243
  Sort the collection according to the given mapping.
240
244
 
@@ -0,0 +1,281 @@
1
+ # SPDX-FileCopyrightText: 2025 EasyScience contributors <core@easyscience.software>
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+ # © 2021-2025 Contributors to the EasyScience project <https://github.com/easyScience/EasyScience
4
+
5
+ from __future__ import annotations
6
+
7
+ import copy
8
+ import warnings
9
+ from collections.abc import MutableSequence
10
+ from typing import Any
11
+ from typing import Callable
12
+ from typing import Dict
13
+ from typing import Iterable
14
+ from typing import List
15
+ from typing import Optional
16
+ from typing import Type
17
+ from typing import TypeVar
18
+ from typing import overload
19
+
20
+ from easyscience.io.serializer_base import SerializerBase
21
+
22
+ from .new_base import NewBase
23
+
24
+ ProtectedType_ = TypeVar('ProtectedType', bound=NewBase)
25
+
26
+
27
+ class EasyList(NewBase, MutableSequence[ProtectedType_]):
28
+ # If we were to inherit from List instead of MutableSequence,
29
+ # we would have to overwrite "extend", "remove", "__iadd__", "count", "append", "__iter__" and "clear"
30
+ def __init__(
31
+ self,
32
+ *args: ProtectedType_ | list[ProtectedType_],
33
+ protected_types: list[Type[NewBase]] | Type[NewBase] | None = None,
34
+ unique_name: Optional[str] = None,
35
+ display_name: Optional[str] = None,
36
+ **kwargs: Any,
37
+ ):
38
+ """
39
+ Initialize the EasyList.
40
+ :param args: Initial items to add to the list
41
+ :param protected_types: Types that are allowed in the list. Can be a single NewBase subclass or a list of them.
42
+ If None, defaults to [NewBase].
43
+ :param unique_name: Optional unique name for the list
44
+ :param display_name: Optional display name for the list
45
+ """
46
+ super().__init__(unique_name=unique_name, display_name=display_name)
47
+ if protected_types is None:
48
+ self._protected_types = [NewBase]
49
+ elif isinstance(protected_types, type) and issubclass(protected_types, NewBase):
50
+ self._protected_types = [protected_types]
51
+ elif isinstance(protected_types, Iterable) and all(issubclass(t, NewBase) for t in protected_types):
52
+ self._protected_types = list(protected_types)
53
+ else:
54
+ raise TypeError('protected_types must be a NewBase subclass or an iterable of NewBase subclasses')
55
+ self._data: List[ProtectedType_] = []
56
+
57
+ # Add initial items
58
+ for item in args:
59
+ if isinstance(item, list):
60
+ for sub_item in item:
61
+ self.append(sub_item)
62
+ else:
63
+ self.append(item)
64
+
65
+ # For deserialization, the dict can't contain an *args, so we check for 'data' in kwargs
66
+ if 'data' in kwargs:
67
+ data = kwargs.pop('data')
68
+ for item in data:
69
+ self.append(item)
70
+
71
+ # MutableSequence abstract methods
72
+
73
+ # Use @overload to provide precise type hints for different __getitem__ argument types
74
+ @overload
75
+ def __getitem__(self, idx: int) -> ProtectedType_: ...
76
+ @overload
77
+ def __getitem__(self, idx: slice) -> 'EasyList[ProtectedType_]': ...
78
+ @overload
79
+ def __getitem__(self, idx: str) -> ProtectedType_: ...
80
+ def __getitem__(self, idx: int | slice | str) -> ProtectedType_ | 'EasyList[ProtectedType_]':
81
+ """
82
+ Get an item by index, slice, or unique_name.
83
+
84
+ :param idx: Index, slice, or unique_name of the item
85
+ :return: The item or a new EasyList for slices
86
+ """
87
+ if isinstance(idx, int):
88
+ return self._data[idx]
89
+ elif isinstance(idx, slice):
90
+ return self.__class__(self._data[idx], protected_types=self._protected_types)
91
+ elif isinstance(idx, str):
92
+ element = next((r for r in self._data if self._get_key(r) == idx), None)
93
+ if element is not None:
94
+ return element
95
+ raise KeyError(f'No item with unique name "{idx}" found')
96
+ else:
97
+ raise TypeError('Index must be an int, slice, or str')
98
+
99
+ @overload
100
+ def __setitem__(self, idx: int, value: ProtectedType_) -> None: ...
101
+ @overload
102
+ def __setitem__(self, idx: slice, value: Iterable[ProtectedType_]) -> None: ...
103
+
104
+ def __setitem__(self, idx: int | slice, value: ProtectedType_ | Iterable[ProtectedType_]) -> None:
105
+ """
106
+ Set an item at an index.
107
+
108
+ :param idx: Index to set
109
+ :param value: New value
110
+ """
111
+ if isinstance(idx, int):
112
+ if not isinstance(value, tuple(self._protected_types)):
113
+ raise TypeError(f'Items must be one of {self._protected_types}, got {type(value)}')
114
+ if value is not self._data[idx] and value in self:
115
+ warnings.warn(f'Item with unique name "{self._get_key(value)}" already in EasyList, it will be ignored')
116
+ return
117
+ self._data[idx] = value
118
+ elif isinstance(idx, slice):
119
+ if not isinstance(value, Iterable):
120
+ raise TypeError('Value must be an iterable for slice assignment')
121
+ replaced = self._data[idx]
122
+ new_values = list(value)
123
+ if len(new_values) != len(replaced):
124
+ raise ValueError('Length of new values must match the length of the slice being replaced')
125
+ for i, v in enumerate(new_values):
126
+ if not isinstance(v, tuple(self._protected_types)):
127
+ raise TypeError(f'Items must be one of {self._protected_types}, got {type(v)}')
128
+ if v in self and replaced[i] is not v:
129
+ warnings.warn(f'Item with unique name "{v.unique_name}" already in EasyList, it will be ignored')
130
+ new_values[i] = replaced[i] # Keep the original value if the new one is a duplicate
131
+ self._data[idx] = new_values
132
+ else:
133
+ raise TypeError('Index must be an int or slice')
134
+
135
+ def __delitem__(self, idx: int | slice | str) -> None:
136
+ """
137
+ Delete an item by index, slice, or name.
138
+
139
+ :param idx: Index, slice, or name of item to delete
140
+ """
141
+ if isinstance(idx, (int, slice)):
142
+ del self._data[idx]
143
+ elif isinstance(idx, str):
144
+ for i, item in enumerate(self._data):
145
+ if self._get_key(item) == idx:
146
+ del self._data[i]
147
+ return
148
+ raise KeyError(f'No item with unique name "{idx}" found')
149
+ else:
150
+ raise TypeError('Index must be an int, slice, or str')
151
+
152
+ def __len__(self) -> int:
153
+ """Return the number of items in the collection."""
154
+ return len(self._data)
155
+
156
+ def insert(self, index: int, value: ProtectedType_) -> None:
157
+ """
158
+ Insert an item at an index.
159
+
160
+ :param index: Index to insert at
161
+ :param value: Item to insert
162
+ """
163
+ if not isinstance(index, int):
164
+ raise TypeError('Index must be an integer')
165
+ elif not isinstance(value, tuple(self._protected_types)):
166
+ raise TypeError(f'Items must be one of {self._protected_types}, got {type(value)}')
167
+ if value in self:
168
+ warnings.warn(f'Item with unique name "{self._get_key(value)}" already in EasyList, it will be ignored')
169
+ return
170
+ self._data.insert(index, value)
171
+
172
+ def _get_key(self, obj) -> str:
173
+ """
174
+ Get the unique name of an object.
175
+ Can be overridden to use a different attribute as the key.
176
+ :param object: Object to get the key for
177
+ :return: The key of the object
178
+ :rtype: str
179
+ """
180
+ return obj.unique_name
181
+
182
+ # Overwriting methods
183
+
184
+ def __repr__(self) -> str:
185
+ return f'{self.__class__.__name__} of length {len(self)} of type(s) {self._protected_types}'
186
+
187
+ def __contains__(self, item: ProtectedType_ | str) -> bool:
188
+ if isinstance(item, str):
189
+ return any(self._get_key(r) == item for r in self._data)
190
+ return item in self._data
191
+
192
+ def __reversed__(self):
193
+ return self._data.__reversed__()
194
+
195
+ def sort(self, key: Callable[[ProtectedType_], Any] = None, reverse: bool = False) -> None:
196
+ """
197
+ Sort the collection according to the given key function.
198
+
199
+ :param key: Mapping function to sort by
200
+ :param reverse: Whether to reverse the sort
201
+ """
202
+ self._data.sort(reverse=reverse, key=key)
203
+
204
+ def index(self, value: ProtectedType_ | str, start: int = 0, stop: int = None) -> int:
205
+ if stop is None:
206
+ stop = len(self._data)
207
+ if isinstance(value, str):
208
+ for i in range(start, min(stop, len(self._data))):
209
+ if self._get_key(self._data[i]) == value:
210
+ return i
211
+ raise ValueError(f'{value} is not in EasyList')
212
+ return self._data.index(value, start, stop)
213
+
214
+ def pop(self, index: int | str = -1) -> ProtectedType_:
215
+ """
216
+ Remove and return an item at the given index or unique_name.
217
+
218
+ :param index: Index or unique_name of the item to remove
219
+ :return: The removed item
220
+ """
221
+ if isinstance(index, int):
222
+ return self._data.pop(index)
223
+ elif isinstance(index, str):
224
+ for i, item in enumerate(self._data):
225
+ if self._get_key(item) == index:
226
+ return self._data.pop(i)
227
+ raise KeyError(f'No item with unique name "{index}" found')
228
+ else:
229
+ raise TypeError('Index must be an int or str')
230
+
231
+ # Serialization support
232
+
233
+ def to_dict(self) -> dict:
234
+ """
235
+ Convert the EasyList to a dictionary for serialization.
236
+
237
+ :return: Dictionary representation of the EasyList
238
+ """
239
+ dict_repr = super().to_dict()
240
+ if self._protected_types != [NewBase]:
241
+ dict_repr['protected_types'] = [
242
+ {'@module': cls_.__module__, '@class': cls_.__name__} for cls_ in self._protected_types
243
+ ] # noqa: E501
244
+ dict_repr['data'] = [item.to_dict() for item in self._data]
245
+ return dict_repr
246
+
247
+ @classmethod
248
+ def from_dict(cls, obj_dict: Dict[str, Any]) -> NewBase:
249
+ """
250
+ Re-create an EasyScience object from a full encoded dictionary.
251
+
252
+ :param obj_dict: dictionary containing the serialized contents (from `SerializerDict`) of an EasyScience object
253
+ :return: Reformed EasyScience object
254
+ """
255
+ if not SerializerBase._is_serialized_easyscience_object(obj_dict):
256
+ raise ValueError('Input must be a dictionary representing an EasyScience EasyList object.')
257
+ temp_dict = copy.deepcopy(obj_dict) # Make a copy to avoid mutating the input
258
+ if temp_dict['@class'] == cls.__name__:
259
+ if 'protected_types' in temp_dict:
260
+ protected_types = temp_dict.pop('protected_types')
261
+ for i, type_dict in enumerate(protected_types):
262
+ if '@module' in type_dict and '@class' in type_dict:
263
+ modname = type_dict['@module']
264
+ classname = type_dict['@class']
265
+ mod = __import__(modname, globals(), locals(), [classname], 0)
266
+ if hasattr(mod, classname):
267
+ cls_ = getattr(mod, classname)
268
+ protected_types[i] = cls_
269
+ else:
270
+ raise ImportError(f'Could not import class {classname} from module {modname}')
271
+ else:
272
+ raise ValueError(
273
+ 'Each protected type must be a serialized EasyScience class with @module and @class keys'
274
+ ) # noqa: E501
275
+ else:
276
+ protected_types = None
277
+ kwargs = SerializerBase.deserialize_dict(temp_dict)
278
+ data = kwargs.pop('data', [])
279
+ return cls(data, protected_types=protected_types, **kwargs)
280
+ else:
281
+ raise ValueError(f'Class name in dictionary does not match the expected class: {cls.__name__}.')
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ # SPDX-FileCopyrightText: 2025 EasyScience contributors <core@easyscience.software>
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ # © 2021-2025 Contributors to the EasyScience project <https://github.com/easyScience/EasyScience
6
+ from typing import TYPE_CHECKING
7
+
8
+ from easyscience.variable.descriptor_number import DescriptorNumber
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Any
12
+ from typing import Dict
13
+ from typing import List
14
+ from typing import Optional
15
+
16
+ from ..io import SerializerBase
17
+ from ..variable import Parameter
18
+ from ..variable.descriptor_base import DescriptorBase
19
+ from .new_base import NewBase
20
+
21
+
22
+ class ModelBase(NewBase):
23
+ """
24
+ This is the base class for all model classes in EasyScience.
25
+ It provides methods to get parameters for fitting and analysis as well as proper serialization/deserialization for
26
+ DescriptorNumber/Parameter attributes.
27
+
28
+ It assumes that Parameters/DescriptorNumbers are assigned as properties with the getters returning the parameter
29
+ but the setter only setting the value of the parameter.
30
+ e.g.
31
+ ```python
32
+ @property
33
+ def my_param(self) -> Parameter:
34
+ return self._my_param
35
+
36
+ @my_param.setter
37
+ def my_param(self, new_value: float) -> None:
38
+ self._my_param.value = new_value
39
+ ```
40
+ """
41
+
42
+ def __init__(self, unique_name: Optional[str] = None, display_name: Optional[str] = None):
43
+ super().__init__(unique_name=unique_name, display_name=display_name)
44
+
45
+ def get_all_variables(self) -> List[DescriptorBase]:
46
+ """
47
+ Get all `Descriptor` and `Parameter` objects as a list.
48
+
49
+ :return: List of `Descriptor` and `Parameter` objects.
50
+ """
51
+ vars = []
52
+ for attr_name in dir(self):
53
+ attr = getattr(self, attr_name)
54
+ if isinstance(attr, DescriptorBase):
55
+ vars.append(attr)
56
+ elif hasattr(attr, 'get_all_variables'):
57
+ vars += attr.get_all_variables()
58
+ return vars
59
+
60
+ def get_all_parameters(self) -> List[Parameter]:
61
+ """
62
+ Get all `Parameter` objects as a list.
63
+
64
+ :return: List of `Parameter` objects.
65
+ """
66
+ return [param for param in self.get_all_variables() if isinstance(param, Parameter)]
67
+
68
+ def get_fittable_parameters(self) -> List[Parameter]:
69
+ """
70
+ Get all parameters which can be fitted as a list.
71
+
72
+ :return: List of `Parameter` objects.
73
+ """
74
+ return [param for param in self.get_all_parameters() if param.independent]
75
+
76
+ def get_free_parameters(self) -> List[Parameter]:
77
+ """
78
+ Get all parameters which are currently free to be fitted as a list.
79
+
80
+ :return: List of `Parameter` objects.
81
+ """
82
+ return [param for param in self.get_fittable_parameters() if not param.fixed]
83
+
84
+ def get_fit_parameters(self) -> List[Parameter]:
85
+ """
86
+ This is an alias for `get_free_parameters`.
87
+ To be removed when fully moved to new base classes and minimizer can be changed.
88
+ """
89
+ return self.get_free_parameters()
90
+
91
+ @classmethod
92
+ def from_dict(cls, obj_dict: Dict[str, Any]) -> ModelBase:
93
+ """
94
+ Re-create an EasyScience object with DescriptorNumber attributes from a full encoded dictionary.
95
+
96
+ :param obj_dict: dictionary containing the serialized contents (from `SerializerDict`) of an EasyScience object
97
+ :return: Reformed EasyScience object
98
+ """
99
+ if not SerializerBase._is_serialized_easyscience_object(obj_dict):
100
+ raise ValueError('Input must be a dictionary representing an EasyScience object.')
101
+ if obj_dict['@class'] == cls.__name__:
102
+ kwargs = SerializerBase.deserialize_dict(obj_dict)
103
+ parameter_placeholder = {}
104
+ for key, value in kwargs.items():
105
+ if isinstance(value, DescriptorNumber):
106
+ parameter_placeholder[key] = value
107
+ kwargs[key] = value.value
108
+ cls_instance = cls(**kwargs)
109
+ for key, value in parameter_placeholder.items():
110
+ try:
111
+ temp_param = getattr(cls_instance, key)
112
+ setattr(cls_instance, '_' + key, value)
113
+ cls_instance._global_object.map.prune(temp_param.unique_name)
114
+ except Exception as e:
115
+ raise SyntaxError(f"""Could not set parameter {key} during `from_dict` with full deserialized variable. \n'
116
+ This should be fixed in the class definition. Error: {e}""") from e
117
+ return cls_instance
118
+ else:
119
+ raise ValueError(f'Class name in dictionary does not match the expected class: {cls.__name__}.')