Skip to content

core

category

CategoryCollection

Bases: CollectionBase

Handles loop-style category containers (e.g. AtomSites).

Each item is a CategoryItem (component).

Source code in src/easydiffraction/core/category.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class CategoryCollection(CollectionBase):
    """Handles loop-style category containers (e.g. AtomSites).

    Each item is a CategoryItem (component).
    """

    def __str__(self) -> str:
        """Human-readable representation of this component."""
        name = self._log_name
        size = len(self)
        return f'<{name} collection ({size} items)>'

    @property
    def unique_name(self):
        return None

    @property
    def parameters(self):
        """All parameters from all items in this collection."""
        params = []
        for item in self._items:
            params.extend(item.parameters)
        return params

    @property
    def as_cif(self) -> str:
        """Return CIF representation of this object."""
        return category_collection_to_cif(self)

    @checktype
    def add(self, item) -> None:
        """Add an item to the collection."""
        self[item._identity.category_entry_name] = item

    @checktype
    def add_from_args(self, *args, **kwargs) -> None:
        """Create and add a new child instance from the provided
        arguments.
        """
        child_obj = self._item_type(*args, **kwargs)
        self.add(child_obj)

__str__()

Human-readable representation of this component.

Source code in src/easydiffraction/core/category.py
48
49
50
51
52
def __str__(self) -> str:
    """Human-readable representation of this component."""
    name = self._log_name
    size = len(self)
    return f'<{name} collection ({size} items)>'

add(item)

Add an item to the collection.

Source code in src/easydiffraction/core/category.py
71
72
73
74
@checktype
def add(self, item) -> None:
    """Add an item to the collection."""
    self[item._identity.category_entry_name] = item

add_from_args(*args, **kwargs)

Create and add a new child instance from the provided arguments.

Source code in src/easydiffraction/core/category.py
76
77
78
79
80
81
82
@checktype
def add_from_args(self, *args, **kwargs) -> None:
    """Create and add a new child instance from the provided
    arguments.
    """
    child_obj = self._item_type(*args, **kwargs)
    self.add(child_obj)

as_cif property

Return CIF representation of this object.

parameters property

All parameters from all items in this collection.

CategoryItem

Bases: GuardedBase

Base class for items in a category collection.

Source code in src/easydiffraction/core/category.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class CategoryItem(GuardedBase):
    """Base class for items in a category collection."""

    def __str__(self) -> str:
        """Human-readable representation of this component."""
        name = self._log_name
        params = ', '.join(f'{p.name}={p.value!r}' for p in self.parameters)
        return f'<{name} ({params})>'

    @property
    def unique_name(self):
        parts = [
            self._identity.datablock_entry_name,
            self._identity.category_code,
            self._identity.category_entry_name,
        ]
        return '.'.join(filter(None, parts))

    @property
    def parameters(self):
        return [v for v in vars(self).values() if isinstance(v, GenericDescriptorBase)]

    @property
    def as_cif(self) -> str:
        """Return CIF representation of this object."""
        return category_item_to_cif(self)

__str__()

Human-readable representation of this component.

Source code in src/easydiffraction/core/category.py
17
18
19
20
21
def __str__(self) -> str:
    """Human-readable representation of this component."""
    name = self._log_name
    params = ', '.join(f'{p.name}={p.value!r}' for p in self.parameters)
    return f'<{name} ({params})>'

as_cif property

Return CIF representation of this object.

collection

Lightweight container for guarded items with name-based indexing.

CollectionBase maintains an ordered list of items and a lazily rebuilt index by the item's identity key. It supports dict-like access for get, set and delete, along with iteration over the items.

CollectionBase

Bases: GuardedBase

A minimal collection with stable iteration and name indexing.

Parameters:

Name Type Description Default
item_type

Type of items accepted by the collection. Used for validation and tooling; not enforced at runtime here.

required
Source code in src/easydiffraction/core/collection.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
class CollectionBase(GuardedBase):
    """A minimal collection with stable iteration and name indexing.

    Args:
        item_type: Type of items accepted by the collection. Used for
            validation and tooling; not enforced at runtime here.
    """

    def __init__(self, item_type) -> None:
        super().__init__()
        self._items: list = []
        self._index: dict = {}
        self._item_type = item_type

    def __getitem__(self, name: str):
        """Return an item by its identity key.

        Rebuilds the internal index on a cache miss to stay consistent
        with recent mutations.
        """
        try:
            return self._index[name]
        except KeyError:
            self._rebuild_index()
            return self._index[name]

    def __setitem__(self, name: str, item) -> None:
        """Insert or replace an item under the given identity key."""
        # Check if item with same identity exists; if so, replace it
        for i, existing_item in enumerate(self._items):
            if existing_item._identity.category_entry_name == name:
                self._items[i] = item
                self._rebuild_index()
                return
        # Otherwise append new item
        item._parent = self  # Explicitly set the parent for the item
        self._items.append(item)
        self._rebuild_index()

    def __delitem__(self, name: str) -> None:
        """Delete an item by key or raise ``KeyError`` if missing."""
        # Remove from _items by identity entry name
        for i, item in enumerate(self._items):
            if item._identity.category_entry_name == name:
                object.__setattr__(item, '_parent', None)  # Unlink the parent before removal
                del self._items[i]
                self._rebuild_index()
                return
        raise KeyError(name)

    def __iter__(self):
        """Iterate over items in insertion order."""
        return iter(self._items)

    def __len__(self) -> int:
        """Return the number of items in the collection."""
        return len(self._items)

    def _key_for(self, item):
        """Return the identity key for ``item`` (category or
        datablock).
        """
        return item._identity.category_entry_name or item._identity.datablock_entry_name

    def _rebuild_index(self) -> None:
        """Rebuild the name-to-item index from the ordered item list."""
        self._index.clear()
        for item in self._items:
            key = self._key_for(item)
            if key:
                self._index[key] = item

    def keys(self):
        """Yield keys for all items in insertion order."""
        return (self._key_for(item) for item in self._items)

    def values(self):
        """Yield items in insertion order."""
        return (item for item in self._items)

    def items(self):
        """Yield ``(key, item)`` pairs in insertion order."""
        return ((self._key_for(item), item) for item in self._items)

    @property
    def names(self):
        """List of all item keys in the collection."""
        return list(self.keys())

__delitem__(name)

Delete an item by key or raise KeyError if missing.

Source code in src/easydiffraction/core/collection.py
54
55
56
57
58
59
60
61
62
63
def __delitem__(self, name: str) -> None:
    """Delete an item by key or raise ``KeyError`` if missing."""
    # Remove from _items by identity entry name
    for i, item in enumerate(self._items):
        if item._identity.category_entry_name == name:
            object.__setattr__(item, '_parent', None)  # Unlink the parent before removal
            del self._items[i]
            self._rebuild_index()
            return
    raise KeyError(name)

__getitem__(name)

Return an item by its identity key.

Rebuilds the internal index on a cache miss to stay consistent with recent mutations.

Source code in src/easydiffraction/core/collection.py
29
30
31
32
33
34
35
36
37
38
39
def __getitem__(self, name: str):
    """Return an item by its identity key.

    Rebuilds the internal index on a cache miss to stay consistent
    with recent mutations.
    """
    try:
        return self._index[name]
    except KeyError:
        self._rebuild_index()
        return self._index[name]

__iter__()

Iterate over items in insertion order.

Source code in src/easydiffraction/core/collection.py
65
66
67
def __iter__(self):
    """Iterate over items in insertion order."""
    return iter(self._items)

__len__()

Return the number of items in the collection.

Source code in src/easydiffraction/core/collection.py
69
70
71
def __len__(self) -> int:
    """Return the number of items in the collection."""
    return len(self._items)

__setitem__(name, item)

Insert or replace an item under the given identity key.

Source code in src/easydiffraction/core/collection.py
41
42
43
44
45
46
47
48
49
50
51
52
def __setitem__(self, name: str, item) -> None:
    """Insert or replace an item under the given identity key."""
    # Check if item with same identity exists; if so, replace it
    for i, existing_item in enumerate(self._items):
        if existing_item._identity.category_entry_name == name:
            self._items[i] = item
            self._rebuild_index()
            return
    # Otherwise append new item
    item._parent = self  # Explicitly set the parent for the item
    self._items.append(item)
    self._rebuild_index()

items()

Yield (key, item) pairs in insertion order.

Source code in src/easydiffraction/core/collection.py
95
96
97
def items(self):
    """Yield ``(key, item)`` pairs in insertion order."""
    return ((self._key_for(item), item) for item in self._items)

keys()

Yield keys for all items in insertion order.

Source code in src/easydiffraction/core/collection.py
87
88
89
def keys(self):
    """Yield keys for all items in insertion order."""
    return (self._key_for(item) for item in self._items)

names property

List of all item keys in the collection.

values()

Yield items in insertion order.

Source code in src/easydiffraction/core/collection.py
91
92
93
def values(self):
    """Yield items in insertion order."""
    return (item for item in self._items)

datablock

DatablockCollection

Bases: CollectionBase

Handles top-level category collections (e.g. SampleModels, Experiments).

Each item is a DatablockItem.

Source code in src/easydiffraction/core/datablock.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class DatablockCollection(CollectionBase):
    """Handles top-level category collections (e.g. SampleModels,
    Experiments).

    Each item is a DatablockItem.
    """

    def __str__(self) -> str:
        """Human-readable representation of this component."""
        name = self._log_name
        size = len(self)
        return f'<{name} collection ({size} items)>'

    @property
    def unique_name(self):
        return None

    @property
    def parameters(self):
        """All parameters from all datablocks in this collection."""
        params = []
        for db in self._items:
            params.extend(db.parameters)
        return params

    # was in class AbstractDatablock(ABC):
    @property
    def fittable_parameters(self) -> list:
        return [p for p in self.parameters if isinstance(p, Parameter) and not p.constrained]

    # was in class AbstractDatablock(ABC):
    @property
    def free_parameters(self) -> list:
        return [p for p in self.fittable_parameters if p.free]

    @property
    def as_cif(self) -> str:
        """Return CIF representation of this object."""
        return datablock_collection_to_cif(self)

    @typechecked
    def add(self, item) -> None:
        """Add an item to the collection."""
        self[item._identity.datablock_entry_name] = item

__str__()

Human-readable representation of this component.

Source code in src/easydiffraction/core/datablock.py
54
55
56
57
58
def __str__(self) -> str:
    """Human-readable representation of this component."""
    name = self._log_name
    size = len(self)
    return f'<{name} collection ({size} items)>'

add(item)

Add an item to the collection.

Source code in src/easydiffraction/core/datablock.py
87
88
89
90
@typechecked
def add(self, item) -> None:
    """Add an item to the collection."""
    self[item._identity.datablock_entry_name] = item

as_cif property

Return CIF representation of this object.

parameters property

All parameters from all datablocks in this collection.

DatablockItem

Bases: GuardedBase

Base class for items in a datablock collection.

Source code in src/easydiffraction/core/datablock.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class DatablockItem(GuardedBase):
    """Base class for items in a datablock collection."""

    def __str__(self) -> str:
        """Human-readable representation of this component."""
        name = self._log_name
        items = getattr(self, '_items', None)
        return f'<{name} ({items})>'

    @property
    def unique_name(self):
        return self._identity.datablock_entry_name

    @property
    def parameters(self):
        """All parameters from all categories contained in this
        datablock.
        """
        params = []
        for v in vars(self).values():
            if isinstance(v, (CategoryItem, CategoryCollection)):
                params.extend(v.parameters)
        return params

    @property
    def as_cif(self) -> str:
        """Return CIF representation of this object."""
        return datablock_item_to_cif(self)

__str__()

Human-readable representation of this component.

Source code in src/easydiffraction/core/datablock.py
20
21
22
23
24
def __str__(self) -> str:
    """Human-readable representation of this component."""
    name = self._log_name
    items = getattr(self, '_items', None)
    return f'<{name} ({items})>'

as_cif property

Return CIF representation of this object.

parameters property

All parameters from all categories contained in this datablock.

diagnostic

Diagnostics helpers for logging validation messages.

This module centralizes human-friendly error and debug logs for attribute validation and configuration checks.

Diagnostics

Centralized logger for attribute errors and validation hints.

Source code in src/easydiffraction/core/diagnostic.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
class Diagnostics:
    """Centralized logger for attribute errors and validation hints."""

    # ==============================================================
    # Configuration / definition diagnostics
    # ==============================================================

    @staticmethod
    def type_override_error(cls_name: str, expected, got):
        """Report an invalid DataTypes override.

        Used when descriptor and AttributeSpec types conflict.
        """
        expected_label = str(expected)
        got_label = str(got)
        msg = (
            f'Invalid type override in <{cls_name}>. '
            f'Descriptor enforces `{expected_label}`, '
            f'but AttributeSpec defines `{got_label}`.'
        )
        Diagnostics._log_error(msg, exc_type=TypeError)

    # ==============================================================
    # Attribute diagnostics
    # ==============================================================

    @staticmethod
    def readonly_error(
        name: str,
        key: str | None = None,
    ):
        """Log an attempt to change a read-only attribute."""
        Diagnostics._log_error(
            f"Cannot modify read-only attribute '{key}' of <{name}>.",
            exc_type=AttributeError,
        )

    @staticmethod
    def attr_error(
        name: str,
        key: str,
        allowed: set[str],
        label='Allowed',
    ):
        """Log access to an unknown attribute and suggest closest
        key.
        """
        suggestion = Diagnostics._build_suggestion(key, allowed)
        # Use consistent (label) logic for allowed
        hint = suggestion or Diagnostics._build_allowed(allowed, label=label)
        Diagnostics._log_error(
            f"Unknown attribute '{key}' of <{name}>.{hint}",
            exc_type=AttributeError,
        )

    # ==============================================================
    # Validation diagnostics
    # ==============================================================

    @staticmethod
    def type_mismatch(
        name: str,
        value,
        expected_type,
        current=None,
        default=None,
    ):
        """Log a type mismatch and keep current or default value."""
        got_type = type(value).__name__
        msg = (
            f'Type mismatch for <{name}>. '
            f'Expected `{expected_type}`, got `{got_type}` ({value!r}).'
        )
        Diagnostics._log_error_with_fallback(
            msg, current=current, default=default, exc_type=TypeError
        )

    @staticmethod
    def range_mismatch(
        name: str,
        value,
        ge,
        le,
        current=None,
        default=None,
    ):
        """Log range violation for a numeric value."""
        msg = f'Value mismatch for <{name}>. Provided {value!r} outside [{ge}, {le}].'
        Diagnostics._log_error_with_fallback(
            msg, current=current, default=default, exc_type=TypeError
        )

    @staticmethod
    def choice_mismatch(
        name: str,
        value,
        allowed,
        current=None,
        default=None,
    ):
        """Log an invalid choice against allowed values."""
        msg = f'Value mismatch for <{name}>. Provided {value!r} is unknown.'
        if allowed is not None:
            msg += Diagnostics._build_allowed(allowed, label='Allowed values')
        Diagnostics._log_error_with_fallback(
            msg, current=current, default=default, exc_type=TypeError
        )

    @staticmethod
    def regex_mismatch(
        name: str,
        value,
        pattern,
        current=None,
        default=None,
    ):
        """Log a regex mismatch with the expected pattern."""
        msg = (
            f"Value mismatch for <{name}>. Provided {value!r} does not match pattern '{pattern}'."
        )
        Diagnostics._log_error_with_fallback(
            msg, current=current, default=default, exc_type=TypeError
        )

    @staticmethod
    def no_value(name, default):
        """Log that default will be used due to missing value."""
        Diagnostics._log_debug(f'No value provided for <{name}>. Using default {default!r}.')

    @staticmethod
    def none_value(name):
        """Log explicit None provided by a user."""
        Diagnostics._log_debug(f'Using `None` explicitly provided for <{name}>.')

    @staticmethod
    def none_value_skip_range(name):
        """Log that range validation is skipped due to None."""
        Diagnostics._log_debug(
            f'Skipping range validation as `None` is explicitly provided for <{name}>.'
        )

    @staticmethod
    def validated(name, value, stage: str | None = None):
        """Log that a value passed a validation stage."""
        stage_info = f' {stage}' if stage else ''
        Diagnostics._log_debug(f'Value {value!r} for <{name}> passed{stage_info} validation.')

    # ==============================================================
    # Helper log methods
    # ==============================================================

    @staticmethod
    def _log_error(msg, exc_type=Exception):
        """Emit an error-level message via shared logger."""
        log.error(msg, exc_type=exc_type)

    @staticmethod
    def _log_error_with_fallback(
        msg,
        current=None,
        default=None,
        exc_type=Exception,
    ):
        """Emit an error message and mention kept or default value."""
        if current is not None:
            msg += f' Keeping current {current!r}.'
        else:
            msg += f' Using default {default!r}.'
        log.error(msg, exc_type=exc_type)

    @staticmethod
    def _log_debug(msg):
        """Emit a debug-level message via shared logger."""
        log.debug(msg)

    # ==============================================================
    # Suggestion and allowed value helpers
    # ==============================================================

    @staticmethod
    def _suggest(key: str, allowed: set[str]):
        """Suggest closest allowed key using string similarity."""
        if not allowed:
            return None
        # Return the allowed key with smallest Levenshtein distance
        matches = difflib.get_close_matches(key, allowed, n=1)
        return matches[0] if matches else None

    @staticmethod
    def _build_suggestion(key: str, allowed: set[str]):
        s = Diagnostics._suggest(key, allowed)
        return f" Did you mean '{s}'?" if s else ''

    @staticmethod
    def _build_allowed(allowed, label='Allowed attributes'):
        # allowed may be a set, list, or other iterable
        if allowed:
            allowed_list = list(allowed)
            if len(allowed_list) <= 10:
                s = ', '.join(map(repr, sorted(allowed_list)))
                return f' {label}: {s}.'
            else:
                return f' ({len(allowed_list)} {label.lower()} not listed here).'
        return ''

attr_error(name, key, allowed, label='Allowed') staticmethod

Log access to an unknown attribute and suggest closest key.

Source code in src/easydiffraction/core/diagnostic.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@staticmethod
def attr_error(
    name: str,
    key: str,
    allowed: set[str],
    label='Allowed',
):
    """Log access to an unknown attribute and suggest closest
    key.
    """
    suggestion = Diagnostics._build_suggestion(key, allowed)
    # Use consistent (label) logic for allowed
    hint = suggestion or Diagnostics._build_allowed(allowed, label=label)
    Diagnostics._log_error(
        f"Unknown attribute '{key}' of <{name}>.{hint}",
        exc_type=AttributeError,
    )

choice_mismatch(name, value, allowed, current=None, default=None) staticmethod

Log an invalid choice against allowed values.

Source code in src/easydiffraction/core/diagnostic.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
@staticmethod
def choice_mismatch(
    name: str,
    value,
    allowed,
    current=None,
    default=None,
):
    """Log an invalid choice against allowed values."""
    msg = f'Value mismatch for <{name}>. Provided {value!r} is unknown.'
    if allowed is not None:
        msg += Diagnostics._build_allowed(allowed, label='Allowed values')
    Diagnostics._log_error_with_fallback(
        msg, current=current, default=default, exc_type=TypeError
    )

no_value(name, default) staticmethod

Log that default will be used due to missing value.

Source code in src/easydiffraction/core/diagnostic.py
138
139
140
141
@staticmethod
def no_value(name, default):
    """Log that default will be used due to missing value."""
    Diagnostics._log_debug(f'No value provided for <{name}>. Using default {default!r}.')

none_value(name) staticmethod

Log explicit None provided by a user.

Source code in src/easydiffraction/core/diagnostic.py
143
144
145
146
@staticmethod
def none_value(name):
    """Log explicit None provided by a user."""
    Diagnostics._log_debug(f'Using `None` explicitly provided for <{name}>.')

none_value_skip_range(name) staticmethod

Log that range validation is skipped due to None.

Source code in src/easydiffraction/core/diagnostic.py
148
149
150
151
152
153
@staticmethod
def none_value_skip_range(name):
    """Log that range validation is skipped due to None."""
    Diagnostics._log_debug(
        f'Skipping range validation as `None` is explicitly provided for <{name}>.'
    )

range_mismatch(name, value, ge, le, current=None, default=None) staticmethod

Log range violation for a numeric value.

Source code in src/easydiffraction/core/diagnostic.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
@staticmethod
def range_mismatch(
    name: str,
    value,
    ge,
    le,
    current=None,
    default=None,
):
    """Log range violation for a numeric value."""
    msg = f'Value mismatch for <{name}>. Provided {value!r} outside [{ge}, {le}].'
    Diagnostics._log_error_with_fallback(
        msg, current=current, default=default, exc_type=TypeError
    )

readonly_error(name, key=None) staticmethod

Log an attempt to change a read-only attribute.

Source code in src/easydiffraction/core/diagnostic.py
40
41
42
43
44
45
46
47
48
49
@staticmethod
def readonly_error(
    name: str,
    key: str | None = None,
):
    """Log an attempt to change a read-only attribute."""
    Diagnostics._log_error(
        f"Cannot modify read-only attribute '{key}' of <{name}>.",
        exc_type=AttributeError,
    )

regex_mismatch(name, value, pattern, current=None, default=None) staticmethod

Log a regex mismatch with the expected pattern.

Source code in src/easydiffraction/core/diagnostic.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
@staticmethod
def regex_mismatch(
    name: str,
    value,
    pattern,
    current=None,
    default=None,
):
    """Log a regex mismatch with the expected pattern."""
    msg = (
        f"Value mismatch for <{name}>. Provided {value!r} does not match pattern '{pattern}'."
    )
    Diagnostics._log_error_with_fallback(
        msg, current=current, default=default, exc_type=TypeError
    )

type_mismatch(name, value, expected_type, current=None, default=None) staticmethod

Log a type mismatch and keep current or default value.

Source code in src/easydiffraction/core/diagnostic.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@staticmethod
def type_mismatch(
    name: str,
    value,
    expected_type,
    current=None,
    default=None,
):
    """Log a type mismatch and keep current or default value."""
    got_type = type(value).__name__
    msg = (
        f'Type mismatch for <{name}>. '
        f'Expected `{expected_type}`, got `{got_type}` ({value!r}).'
    )
    Diagnostics._log_error_with_fallback(
        msg, current=current, default=default, exc_type=TypeError
    )

type_override_error(cls_name, expected, got) staticmethod

Report an invalid DataTypes override.

Used when descriptor and AttributeSpec types conflict.

Source code in src/easydiffraction/core/diagnostic.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@staticmethod
def type_override_error(cls_name: str, expected, got):
    """Report an invalid DataTypes override.

    Used when descriptor and AttributeSpec types conflict.
    """
    expected_label = str(expected)
    got_label = str(got)
    msg = (
        f'Invalid type override in <{cls_name}>. '
        f'Descriptor enforces `{expected_label}`, '
        f'but AttributeSpec defines `{got_label}`.'
    )
    Diagnostics._log_error(msg, exc_type=TypeError)

validated(name, value, stage=None) staticmethod

Log that a value passed a validation stage.

Source code in src/easydiffraction/core/diagnostic.py
155
156
157
158
159
@staticmethod
def validated(name, value, stage: str | None = None):
    """Log that a value passed a validation stage."""
    stage_info = f' {stage}' if stage else ''
    Diagnostics._log_debug(f'Value {value!r} for <{name}> passed{stage_info} validation.')

factory

FactoryBase

Reusable argument validation mixin.

Source code in src/easydiffraction/core/factory.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class FactoryBase:
    """Reusable argument validation mixin."""

    @staticmethod
    def _validate_args(
        present: set[str],
        allowed_specs: Iterable[Mapping[str, Iterable[str]]],
        factory_name: str,
    ) -> None:
        """Validate provided arguments against allowed combinations."""
        for spec in allowed_specs:
            required = set(spec.get('required', []))
            optional = set(spec.get('optional', []))
            if required.issubset(present) and present <= (required | optional):
                return  # valid combo
        # build readable error message
        combos = []
        for spec in allowed_specs:
            req = ', '.join(spec.get('required', []))
            opt = ', '.join(spec.get('optional', []))
            if opt:
                combos.append(f'({req}[, {opt}])')
            else:
                combos.append(f'({req})')
        raise ValueError(
            f'Invalid argument combination for {factory_name} creation.\n'
            f'Provided: {sorted(present)}\n'
            f'Allowed combinations:\n  ' + '\n  '.join(combos)
        )

guard

GuardedBase

Bases: ABC

Base class enforcing controlled attribute access and parent linkage.

Source code in src/easydiffraction/core/guard.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class GuardedBase(ABC):
    """Base class enforcing controlled attribute access and parent
    linkage.
    """

    # 5b: Use class-level diagnoser
    _diagnoser = Diagnostics()

    def __init__(self):
        self._identity = Identity(owner=self)

    def __str__(self) -> str:
        return f'<{self.unique_name}>'

    def __repr__(self) -> str:
        return self.__str__()

    def __getattr__(self, key: str):
        cls = type(self)
        allowed = cls._public_attrs()
        if key not in allowed:
            type(self)._diagnoser.attr_error(
                self.unique_name,
                key,
                allowed,
                label='Allowed readable/writable',
            )

    def __setattr__(self, key: str, value):
        # Always allow private or special attributes without diagnostics
        if key.startswith('_'):
            object.__setattr__(self, key, value)
            # Also maintain parent linkage for nested objects
            if key != '_parent' and isinstance(value, GuardedBase):
                object.__setattr__(value, '_parent', self)
            return

        # Handle public attributes with diagnostics
        cls = type(self)
        # Prevent modification of read-only attributes
        if key in cls._public_readonly_attrs():
            cls._diagnoser.readonly_error(
                self.unique_name,
                key,
            )
            return
        # Prevent assignment to unknown attributes
        # Show writable attributes only as allowed
        if key not in cls._public_attrs():
            allowed = cls._public_writable_attrs()
            cls._diagnoser.attr_error(
                self.unique_name,
                key,
                allowed,
                label='Allowed writable',
            )
            return

        self._assign_attr(key, value)

    def _assign_attr(self, key, value):
        """Low-level assignment with parent linkage."""
        object.__setattr__(self, key, value)
        if key != '_parent' and isinstance(value, GuardedBase):
            object.__setattr__(value, '_parent', self)

    @classmethod
    def _iter_properties(cls):
        """Iterate over all public properties defined in the class
        hierarchy.

        Yields:
            tuple[str, property]: Each (key, property) pair for public
            attributes.
        """
        for base in cls.mro():
            for key, attr in base.__dict__.items():
                if key.startswith('_') or not isinstance(attr, property):
                    continue
                yield key, attr

    @classmethod
    def _public_attrs(cls):
        """All public properties (read-only + writable)."""
        return {key for key, _ in cls._iter_properties()}

    @classmethod
    def _public_readonly_attrs(cls):
        """Public properties without a setter."""
        return {key for key, prop in cls._iter_properties() if prop.fset is None}

    @classmethod
    def _public_writable_attrs(cls) -> set[str]:
        """Public properties with a setter."""
        return {key for key, prop in cls._iter_properties() if prop.fset is not None}

    def _allowed_attrs(self, writable_only=False):
        cls = type(self)
        if writable_only:
            return cls._public_writable_attrs()
        return cls._public_attrs()

    @property
    def _log_name(self):
        return type(self).__name__

    @property
    def unique_name(self):
        return type(self).__name__

    # @property
    # def identity(self):
    #    """Expose a limited read-only view of identity attributes."""
    #    return SimpleNamespace(
    #        datablock_entry_name=self._identity.datablock_entry_name,
    #        category_code=self._identity.category_code,
    #        category_entry_name=self._identity.category_entry_name,
    #    )

    @property
    @abstractmethod
    def parameters(self):
        """Return a list of parameter objects (to be implemented by
        subclasses).
        """
        raise NotImplementedError

    @property
    @abstractmethod
    def as_cif(self) -> str:
        """Return CIF representation of this object (to be implemented
        by subclasses).
        """
        raise NotImplementedError

as_cif abstractmethod property

Return CIF representation of this object (to be implemented by subclasses).

parameters abstractmethod property

Return a list of parameter objects (to be implemented by subclasses).

identity

Identity helpers to build CIF-like hierarchical names.

Used by containers and items to expose datablock/category/entry names without tight coupling.

Identity

Resolve datablock/category/entry relationships lazily.

Source code in src/easydiffraction/core/identity.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class Identity:
    """Resolve datablock/category/entry relationships lazily."""

    def __init__(
        self,
        *,
        owner: object,
        datablock_entry: Callable | None = None,
        category_code: str | None = None,
        category_entry: Callable | None = None,
    ):
        self._owner = owner
        self._datablock_entry = datablock_entry
        self._category_code = category_code
        self._category_entry = category_entry

    def _resolve_up(self, attr: str, visited=None):
        """Resolve attribute by walking up parent chain safely."""
        if visited is None:
            visited = set()
        if id(self) in visited:
            return None
        visited.add(id(self))

        # Direct callable or value on self
        value = getattr(self, f'_{attr}', None)
        if callable(value):
            return value()
        if isinstance(value, str):
            return value

        # Climb to parent if available
        parent = getattr(self._owner, '__dict__', {}).get('_parent')
        if parent and hasattr(parent, '_identity'):
            return parent._identity._resolve_up(attr, visited)
        return None

    @property
    def datablock_entry_name(self):
        """Datablock entry name or None if not set."""
        return self._resolve_up('datablock_entry')

    @datablock_entry_name.setter
    def datablock_entry_name(self, func: callable):
        """Set callable returning datablock entry name."""
        self._datablock_entry = func

    @property
    def category_code(self):
        """Category code like 'atom_site' or 'background'."""
        return self._resolve_up('category_code')

    @category_code.setter
    def category_code(self, value: str):
        """Set category code value."""
        self._category_code = value

    @property
    def category_entry_name(self):
        """Category entry name or None if not set."""
        return self._resolve_up('category_entry')

    @category_entry_name.setter
    def category_entry_name(self, func: callable):
        """Set callable returning category entry name."""
        self._category_entry = func

category_code property writable

Category code like 'atom_site' or 'background'.

category_entry_name property writable

Category entry name or None if not set.

datablock_entry_name property writable

Datablock entry name or None if not set.

parameters

GenericDescriptorBase

Bases: GuardedBase

Base class for all parameter-like descriptors.

A descriptor encapsulates a typed value with validation, human-readable name/description and a globally unique identifier that is stable across the session. Concrete subclasses specialize the expected data type and can extend the public API with additional behavior (e.g. units).

Attributes:

Name Type Description
name str

Local parameter name (e.g. 'a', 'b_iso').

description

Optional human-readable description.

uid

Stable random identifier for external references.

Source code in src/easydiffraction/core/parameters.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
class GenericDescriptorBase(GuardedBase):
    """Base class for all parameter-like descriptors.

    A descriptor encapsulates a typed value with validation,
    human-readable name/description and a globally unique identifier
    that is stable across the session. Concrete subclasses specialize
    the expected data type and can extend the public API with
    additional behavior (e.g. units).

    Attributes:
        name: Local parameter name (e.g. 'a', 'b_iso').
        description: Optional human-readable description.
        uid: Stable random identifier for external references.
    """

    _BOOL_SPEC_TEMPLATE = AttributeSpec(
        type_=DataTypes.BOOL,
        default=False,
    )

    def __init__(
        self,
        *,
        value_spec: AttributeSpec,
        name: str,
        description: str = None,
    ):
        """Initialize the descriptor with validation and identity.

        Args:
            value_spec: Validation specification for the value.
            name: Local name of the descriptor within its category.
            description: Optional human-readable description.
        """
        super().__init__()

        expected_type = getattr(self, '_value_type', None)

        if expected_type:
            user_type = (
                value_spec._type_validator.expected_type
                if value_spec._type_validator is not None
                else None
            )
            if user_type and user_type is not expected_type:
                Diagnostics.type_override_error(
                    type(self).__name__,
                    expected_type,
                    user_type,
                )
            else:
                # Enforce descriptor's own type if not already defined
                value_spec._type_validator = TypeValidator(expected_type)

        self._value_spec = value_spec
        self._name = name
        self._description = description
        self._uid: str = self._generate_uid()
        UidMapHandler.get().add_to_uid_map(self)

        # Initial validated states
        self._value = self._value_spec.validated(
            value_spec.value,
            name=self.unique_name,
        )

    def __str__(self) -> str:
        return f'<{self.unique_name} = {self.value!r}>'

    @staticmethod
    def _generate_uid(length: int = 16) -> str:
        letters = string.ascii_lowercase
        return ''.join(secrets.choice(letters) for _ in range(length))

    @property
    def uid(self):
        """Stable random identifier for this descriptor."""
        return self._uid

    @property
    def name(self) -> str:
        """Local name of the descriptor (without category/datablock)."""
        return self._name

    @property
    def unique_name(self):
        """Fully qualified name including datablock, category and entry
        name.
        """
        # 7c: Use filter(None, [...])
        parts = [
            self._identity.datablock_entry_name,
            self._identity.category_code,
            self._identity.category_entry_name,
            self.name,
        ]
        return '.'.join(filter(None, parts))

    @property
    def value(self):
        """Current validated value."""
        return self._value

    @value.setter
    def value(self, v):
        """Set a new value after validating against the spec."""
        self._value = self._value_spec.validated(
            v,
            name=self.unique_name,
            current=self._value,
        )

    @property
    def description(self):
        """Optional human-readable description."""
        return self._description

    @property
    def parameters(self):
        """Return a flat list of parameters contained by this object.

        For a single descriptor, it returns a one-element list with
        itself. Composite objects override this to flatten nested
        structures.
        """
        return [self]

    @property
    def as_cif(self) -> str:
        """Serialize this descriptor to a CIF-formatted string."""
        return param_to_cif(self)

__init__(*, value_spec, name, description=None)

Initialize the descriptor with validation and identity.

Parameters:

Name Type Description Default
value_spec AttributeSpec

Validation specification for the value.

required
name str

Local name of the descriptor within its category.

required
description str

Optional human-readable description.

None
Source code in src/easydiffraction/core/parameters.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def __init__(
    self,
    *,
    value_spec: AttributeSpec,
    name: str,
    description: str = None,
):
    """Initialize the descriptor with validation and identity.

    Args:
        value_spec: Validation specification for the value.
        name: Local name of the descriptor within its category.
        description: Optional human-readable description.
    """
    super().__init__()

    expected_type = getattr(self, '_value_type', None)

    if expected_type:
        user_type = (
            value_spec._type_validator.expected_type
            if value_spec._type_validator is not None
            else None
        )
        if user_type and user_type is not expected_type:
            Diagnostics.type_override_error(
                type(self).__name__,
                expected_type,
                user_type,
            )
        else:
            # Enforce descriptor's own type if not already defined
            value_spec._type_validator = TypeValidator(expected_type)

    self._value_spec = value_spec
    self._name = name
    self._description = description
    self._uid: str = self._generate_uid()
    UidMapHandler.get().add_to_uid_map(self)

    # Initial validated states
    self._value = self._value_spec.validated(
        value_spec.value,
        name=self.unique_name,
    )

as_cif property

Serialize this descriptor to a CIF-formatted string.

description property

Optional human-readable description.

name property

Local name of the descriptor (without category/datablock).

parameters property

Return a flat list of parameters contained by this object.

For a single descriptor, it returns a one-element list with itself. Composite objects override this to flatten nested structures.

uid property

Stable random identifier for this descriptor.

unique_name property

Fully qualified name including datablock, category and entry name.

value property writable

Current validated value.

GenericNumericDescriptor

Bases: GenericDescriptorBase

Source code in src/easydiffraction/core/parameters.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
class GenericNumericDescriptor(GenericDescriptorBase):
    _value_type = DataTypes.NUMERIC

    def __init__(
        self,
        *,
        units: str = '',
        **kwargs: Any,
    ) -> None:
        super().__init__(**kwargs)
        self._units: str = units

    def __str__(self) -> str:
        s: str = super().__str__()
        s = s[1:-1]  # strip <>
        if self.units:
            s += f' {self.units}'
        return f'<{s}>'

    @property
    def units(self) -> str:
        """Units associated with the numeric value, if any."""
        return self._units

units property

Units associated with the numeric value, if any.

GenericParameter

Bases: GenericNumericDescriptor

Numeric descriptor extended with fitting-related attributes.

Adds standard attributes used by minimizers: "free" flag, uncertainty, bounds and an optional starting value. Subclasses can integrate with specific backends while preserving this interface.

Source code in src/easydiffraction/core/parameters.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
class GenericParameter(GenericNumericDescriptor):
    """Numeric descriptor extended with fitting-related attributes.

    Adds standard attributes used by minimizers: "free" flag,
    uncertainty, bounds and an optional starting value. Subclasses can
    integrate with specific backends while preserving this interface.
    """

    def __init__(
        self,
        **kwargs: Any,
    ):
        super().__init__(**kwargs)

        # Initial validated states
        self._free_spec = self._BOOL_SPEC_TEMPLATE
        self._free = self._free_spec.default
        self._uncertainty_spec = AttributeSpec(
            type_=DataTypes.NUMERIC,
            content_validator=RangeValidator(ge=0),
            allow_none=True,
        )
        self._uncertainty = self._uncertainty_spec.default
        self._fit_min_spec = AttributeSpec(type_=DataTypes.NUMERIC, default=-np.inf)
        self._fit_min = self._fit_min_spec.default
        self._fit_max_spec = AttributeSpec(type_=DataTypes.NUMERIC, default=np.inf)
        self._fit_max = self._fit_max_spec.default
        self._start_value_spec = AttributeSpec(type_=DataTypes.NUMERIC, default=0.0)
        self._start_value = self._start_value_spec.default
        self._constrained_spec = self._BOOL_SPEC_TEMPLATE
        self._constrained = self._constrained_spec.default

    def __str__(self) -> str:
        s = GenericDescriptorBase.__str__(self)
        s = s[1:-1]  # strip <>
        if self.uncertainty is not None:
            s += f' ± {self.uncertainty}'
        if self.units is not None:
            s += f' {self.units}'
        s += f' (free={self.free})'
        return f'<{s}>'

    @property
    def _minimizer_uid(self):
        """Variant of uid that is safe for minimizer engines."""
        # return self.unique_name.replace('.', '__')
        return self.uid

    @property
    def name(self) -> str:
        """Local name of the parameter (without category/datablock)."""
        return self._name

    @property
    def unique_name(self):
        """Fully qualified parameter name including its context path."""
        parts = [
            self._identity.datablock_entry_name,
            self._identity.category_code,
            self._identity.category_entry_name,
            self.name,
        ]
        return '.'.join(filter(None, parts))

    @property
    def constrained(self):
        """Whether this parameter is part of a constraint expression."""
        return self._constrained

    @property
    def free(self):
        """Whether this parameter is currently varied during fitting."""
        return self._free

    @free.setter
    def free(self, v):
        """Set the "free" flag after validation."""
        self._free = self._free_spec.validated(
            v, name=f'{self.unique_name}.free', current=self._free
        )

    @property
    def uncertainty(self):
        """Estimated standard uncertainty of the fitted value, if
        available.
        """
        return self._uncertainty

    @uncertainty.setter
    def uncertainty(self, v):
        """Set the uncertainty value (must be non-negative or None)."""
        self._uncertainty = self._uncertainty_spec.validated(
            v, name=f'{self.unique_name}.uncertainty', current=self._uncertainty
        )

    @property
    def fit_min(self):
        """Lower fitting bound."""
        return self._fit_min

    @fit_min.setter
    def fit_min(self, v):
        """Set the lower bound for the parameter value."""
        self._fit_min = self._fit_min_spec.validated(
            v, name=f'{self.unique_name}.fit_min', current=self._fit_min
        )

    @property
    def fit_max(self):
        """Upper fitting bound."""
        return self._fit_max

    @fit_max.setter
    def fit_max(self, v):
        """Set the upper bound for the parameter value."""
        self._fit_max = self._fit_max_spec.validated(
            v, name=f'{self.unique_name}.fit_max', current=self._fit_max
        )

constrained property

Whether this parameter is part of a constraint expression.

fit_max property writable

Upper fitting bound.

fit_min property writable

Lower fitting bound.

free property writable

Whether this parameter is currently varied during fitting.

name property

Local name of the parameter (without category/datablock).

uncertainty property writable

Estimated standard uncertainty of the fitted value, if available.

unique_name property

Fully qualified parameter name including its context path.

NumericDescriptor

Bases: GenericNumericDescriptor

Source code in src/easydiffraction/core/parameters.py
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
class NumericDescriptor(GenericNumericDescriptor):
    def __init__(
        self,
        *,
        cif_handler: CifHandler,
        **kwargs: Any,
    ) -> None:
        """Numeric descriptor bound to a CIF handler.

        Args:
            cif_handler: Object that tracks CIF identifiers.
            **kwargs: Forwarded to GenericNumericDescriptor.
        """
        super().__init__(**kwargs)
        self._cif_handler = cif_handler
        self._cif_handler.attach(self)

__init__(*, cif_handler, **kwargs)

Numeric descriptor bound to a CIF handler.

Parameters:

Name Type Description Default
cif_handler CifHandler

Object that tracks CIF identifiers.

required
**kwargs Any

Forwarded to GenericNumericDescriptor.

{}
Source code in src/easydiffraction/core/parameters.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
def __init__(
    self,
    *,
    cif_handler: CifHandler,
    **kwargs: Any,
) -> None:
    """Numeric descriptor bound to a CIF handler.

    Args:
        cif_handler: Object that tracks CIF identifiers.
        **kwargs: Forwarded to GenericNumericDescriptor.
    """
    super().__init__(**kwargs)
    self._cif_handler = cif_handler
    self._cif_handler.attach(self)

Parameter

Bases: GenericParameter

Source code in src/easydiffraction/core/parameters.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
class Parameter(GenericParameter):
    def __init__(
        self,
        *,
        cif_handler: CifHandler,
        **kwargs: Any,
    ) -> None:
        """Fittable parameter bound to a CIF handler.

        Args:
            cif_handler: Object that tracks CIF identifiers.
            **kwargs: Forwarded to GenericParameter.
        """
        super().__init__(**kwargs)
        self._cif_handler = cif_handler
        self._cif_handler.attach(self)

__init__(*, cif_handler, **kwargs)

Fittable parameter bound to a CIF handler.

Parameters:

Name Type Description Default
cif_handler CifHandler

Object that tracks CIF identifiers.

required
**kwargs Any

Forwarded to GenericParameter.

{}
Source code in src/easydiffraction/core/parameters.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
def __init__(
    self,
    *,
    cif_handler: CifHandler,
    **kwargs: Any,
) -> None:
    """Fittable parameter bound to a CIF handler.

    Args:
        cif_handler: Object that tracks CIF identifiers.
        **kwargs: Forwarded to GenericParameter.
    """
    super().__init__(**kwargs)
    self._cif_handler = cif_handler
    self._cif_handler.attach(self)

StringDescriptor

Bases: GenericStringDescriptor

Source code in src/easydiffraction/core/parameters.py
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
class StringDescriptor(GenericStringDescriptor):
    def __init__(
        self,
        *,
        cif_handler: CifHandler,
        **kwargs: Any,
    ) -> None:
        """String descriptor bound to a CIF handler.

        Args:
            cif_handler: Object that tracks CIF identifiers.
            **kwargs: Forwarded to GenericStringDescriptor.
        """
        super().__init__(**kwargs)
        self._cif_handler = cif_handler
        self._cif_handler.attach(self)

__init__(*, cif_handler, **kwargs)

String descriptor bound to a CIF handler.

Parameters:

Name Type Description Default
cif_handler CifHandler

Object that tracks CIF identifiers.

required
**kwargs Any

Forwarded to GenericStringDescriptor.

{}
Source code in src/easydiffraction/core/parameters.py
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
def __init__(
    self,
    *,
    cif_handler: CifHandler,
    **kwargs: Any,
) -> None:
    """String descriptor bound to a CIF handler.

    Args:
        cif_handler: Object that tracks CIF identifiers.
        **kwargs: Forwarded to GenericStringDescriptor.
    """
    super().__init__(**kwargs)
    self._cif_handler = cif_handler
    self._cif_handler.attach(self)

singletons

ConstraintsHandler

Bases: SingletonBase

Manages user-defined parameter constraints using aliases and expressions.

Uses the asteval interpreter for safe evaluation of mathematical expressions. Constraints are defined as: lhs_alias = expression(rhs_aliases).

Source code in src/easydiffraction/core/singletons.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
class ConstraintsHandler(SingletonBase):
    """Manages user-defined parameter constraints using aliases and
    expressions.

    Uses the asteval interpreter for safe evaluation of mathematical
    expressions. Constraints are defined as: lhs_alias =
    expression(rhs_aliases).
    """

    def __init__(self) -> None:
        # Maps alias names
        # (like 'biso_La') → ConstraintAlias(param=Parameter)
        self._alias_to_param: Dict[str, Any] = {}

        # Stores raw user-defined constraints indexed by lhs_alias
        # Each value should contain: lhs_alias, rhs_expr
        self._constraints = {}

        # Internally parsed constraints as (lhs_alias, rhs_expr) tuples
        self._parsed_constraints: List[Tuple[str, str]] = []

    def set_aliases(self, aliases):
        """Sets the alias map (name → parameter wrapper).

        Called when user registers parameter aliases like:
            alias='biso_La', param=model.atom_sites['La'].b_iso
        """
        self._alias_to_param = dict(aliases.items())

    def set_constraints(self, constraints):
        """Sets the constraints and triggers parsing into internal
        format.

        Called when user registers expressions like:
            lhs_alias='occ_Ba', rhs_expr='1 - occ_La'
        """
        self._constraints = constraints._items
        self._parse_constraints()

    def _parse_constraints(self) -> None:
        """Converts raw expression input into a normalized internal list
        of (lhs_alias, rhs_expr) pairs, stripping whitespace and
        skipping invalid entries.
        """
        self._parsed_constraints = []

        for expr_obj in self._constraints:
            lhs_alias = expr_obj.lhs_alias.value
            rhs_expr = expr_obj.rhs_expr.value

            if lhs_alias and rhs_expr:
                constraint = (lhs_alias.strip(), rhs_expr.strip())
                self._parsed_constraints.append(constraint)

    def apply(self) -> None:
        """Evaluates constraints and applies them to dependent
        parameters.

        For each constraint:
        - Evaluate RHS using current values of aliases
        - Locate the dependent parameter by alias → uid → param
        - Update its value and mark it as constrained
        """
        if not self._parsed_constraints:
            return  # Nothing to apply

        # Retrieve global UID → Parameter object map
        uid_map = UidMapHandler.get().get_uid_map()

        # Prepare a flat dict of {alias: value} for use in expressions
        param_values = {}
        for alias, alias_obj in self._alias_to_param.items():
            uid = alias_obj.param_uid.value
            param = uid_map[uid]
            value = param.value
            param_values[alias] = value

        # Create an asteval interpreter for safe expression evaluation
        ae = Interpreter()
        ae.symtable.update(param_values)

        for lhs_alias, rhs_expr in self._parsed_constraints:
            try:
                # Evaluate the RHS expression using the current values
                rhs_value = ae(rhs_expr)

                # Get the actual parameter object we want to update
                dependent_uid = self._alias_to_param[lhs_alias].param_uid.value
                param = uid_map[dependent_uid]

                # Update its value and mark it as constrained
                param._value = rhs_value  # To bypass ranges check
                param._constrained = True  # To bypass read-only check

            except Exception as error:
                print(f"Failed to apply constraint '{lhs_alias} = {rhs_expr}': {error}")

apply()

Evaluates constraints and applies them to dependent parameters.

For each constraint: - Evaluate RHS using current values of aliases - Locate the dependent parameter by alias → uid → param - Update its value and mark it as constrained

Source code in src/easydiffraction/core/singletons.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def apply(self) -> None:
    """Evaluates constraints and applies them to dependent
    parameters.

    For each constraint:
    - Evaluate RHS using current values of aliases
    - Locate the dependent parameter by alias → uid → param
    - Update its value and mark it as constrained
    """
    if not self._parsed_constraints:
        return  # Nothing to apply

    # Retrieve global UID → Parameter object map
    uid_map = UidMapHandler.get().get_uid_map()

    # Prepare a flat dict of {alias: value} for use in expressions
    param_values = {}
    for alias, alias_obj in self._alias_to_param.items():
        uid = alias_obj.param_uid.value
        param = uid_map[uid]
        value = param.value
        param_values[alias] = value

    # Create an asteval interpreter for safe expression evaluation
    ae = Interpreter()
    ae.symtable.update(param_values)

    for lhs_alias, rhs_expr in self._parsed_constraints:
        try:
            # Evaluate the RHS expression using the current values
            rhs_value = ae(rhs_expr)

            # Get the actual parameter object we want to update
            dependent_uid = self._alias_to_param[lhs_alias].param_uid.value
            param = uid_map[dependent_uid]

            # Update its value and mark it as constrained
            param._value = rhs_value  # To bypass ranges check
            param._constrained = True  # To bypass read-only check

        except Exception as error:
            print(f"Failed to apply constraint '{lhs_alias} = {rhs_expr}': {error}")

set_aliases(aliases)

Sets the alias map (name → parameter wrapper).

Called when user registers parameter aliases like

alias='biso_La', param=model.atom_sites['La'].b_iso

Source code in src/easydiffraction/core/singletons.py
 97
 98
 99
100
101
102
103
def set_aliases(self, aliases):
    """Sets the alias map (name → parameter wrapper).

    Called when user registers parameter aliases like:
        alias='biso_La', param=model.atom_sites['La'].b_iso
    """
    self._alias_to_param = dict(aliases.items())

set_constraints(constraints)

Sets the constraints and triggers parsing into internal format.

Called when user registers expressions like

lhs_alias='occ_Ba', rhs_expr='1 - occ_La'

Source code in src/easydiffraction/core/singletons.py
105
106
107
108
109
110
111
112
113
def set_constraints(self, constraints):
    """Sets the constraints and triggers parsing into internal
    format.

    Called when user registers expressions like:
        lhs_alias='occ_Ba', rhs_expr='1 - occ_La'
    """
    self._constraints = constraints._items
    self._parse_constraints()

SingletonBase

Base class to implement Singleton pattern.

Ensures only one shared instance of a class is ever created. Useful for managing shared state across the library.

Source code in src/easydiffraction/core/singletons.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class SingletonBase:
    """Base class to implement Singleton pattern.

    Ensures only one shared instance of a class is ever created. Useful
    for managing shared state across the library.
    """

    _instance = None  # Class-level shared instance

    @classmethod
    def get(cls: Type[T]) -> T:
        """Returns the shared instance, creating it if needed."""
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

get() classmethod

Returns the shared instance, creating it if needed.

Source code in src/easydiffraction/core/singletons.py
25
26
27
28
29
30
@classmethod
def get(cls: Type[T]) -> T:
    """Returns the shared instance, creating it if needed."""
    if cls._instance is None:
        cls._instance = cls()
    return cls._instance

UidMapHandler

Bases: SingletonBase

Global handler to manage UID-to-Parameter object mapping.

Source code in src/easydiffraction/core/singletons.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class UidMapHandler(SingletonBase):
    """Global handler to manage UID-to-Parameter object mapping."""

    def __init__(self) -> None:
        # Internal map: uid (str) → Parameter instance
        self._uid_map: Dict[str, Any] = {}

    def get_uid_map(self) -> Dict[str, Any]:
        """Returns the current UID-to-Parameter map."""
        return self._uid_map

    def add_to_uid_map(self, parameter):
        """Adds a single Parameter or Descriptor object to the UID map.

        Only Descriptor or Parameter instances are allowed (not
        Components or others).
        """
        from easydiffraction.core.parameters import GenericDescriptorBase

        if not isinstance(parameter, GenericDescriptorBase):
            raise TypeError(
                f'Cannot add object of type {type(parameter).__name__} to UID map. '
                'Only Descriptor or Parameter instances are allowed.'
            )
        self._uid_map[parameter.uid] = parameter

    def replace_uid(self, old_uid, new_uid):
        """Replaces an existing UID key in the UID map with a new UID.

        Moves the associated parameter from old_uid to new_uid. Raises a
        KeyError if the old_uid doesn't exist.
        """
        if old_uid not in self._uid_map:
            # Only raise if old_uid is not None and not empty
            print('DEBUG: replace_uid failed', old_uid, 'current map:', list(self._uid_map.keys()))
            raise KeyError(f"UID '{old_uid}' not found in the UID map.")
        self._uid_map[new_uid] = self._uid_map.pop(old_uid)

add_to_uid_map(parameter)

Adds a single Parameter or Descriptor object to the UID map.

Only Descriptor or Parameter instances are allowed (not Components or others).

Source code in src/easydiffraction/core/singletons.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def add_to_uid_map(self, parameter):
    """Adds a single Parameter or Descriptor object to the UID map.

    Only Descriptor or Parameter instances are allowed (not
    Components or others).
    """
    from easydiffraction.core.parameters import GenericDescriptorBase

    if not isinstance(parameter, GenericDescriptorBase):
        raise TypeError(
            f'Cannot add object of type {type(parameter).__name__} to UID map. '
            'Only Descriptor or Parameter instances are allowed.'
        )
    self._uid_map[parameter.uid] = parameter

get_uid_map()

Returns the current UID-to-Parameter map.

Source code in src/easydiffraction/core/singletons.py
40
41
42
def get_uid_map(self) -> Dict[str, Any]:
    """Returns the current UID-to-Parameter map."""
    return self._uid_map

replace_uid(old_uid, new_uid)

Replaces an existing UID key in the UID map with a new UID.

Moves the associated parameter from old_uid to new_uid. Raises a KeyError if the old_uid doesn't exist.

Source code in src/easydiffraction/core/singletons.py
59
60
61
62
63
64
65
66
67
68
69
def replace_uid(self, old_uid, new_uid):
    """Replaces an existing UID key in the UID map with a new UID.

    Moves the associated parameter from old_uid to new_uid. Raises a
    KeyError if the old_uid doesn't exist.
    """
    if old_uid not in self._uid_map:
        # Only raise if old_uid is not None and not empty
        print('DEBUG: replace_uid failed', old_uid, 'current map:', list(self._uid_map.keys()))
        raise KeyError(f"UID '{old_uid}' not found in the UID map.")
    self._uid_map[new_uid] = self._uid_map.pop(old_uid)

validation

Lightweight runtime validation utilities.

Provides DataTypes, type/content validators, and AttributeSpec used by descriptors and parameters. Only documentation was added here.

AttributeSpec

Hold metadata and validators for a single attribute.

Source code in src/easydiffraction/core/validation.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
class AttributeSpec:
    """Hold metadata and validators for a single attribute."""

    def __init__(
        self,
        *,
        value=None,
        type_=None,
        default=None,
        content_validator=None,
        allow_none: bool = False,
    ):
        self.value = value
        self.default = default
        self.allow_none = allow_none
        self._type_validator = TypeValidator(type_) if type_ else None
        self._content_validator = content_validator

    def validated(
        self,
        value,
        name,
        current=None,
    ):
        """Validate through type and content validators.

        Returns validated value, possibly default or current if errors
        occur. None may short-circuit further checks when allowed.
        """
        val = value
        # Evaluate callable defaults dynamically
        default = self.default() if callable(self.default) else self.default

        # Type validation
        if self._type_validator:
            val = self._type_validator.validated(
                val,
                name,
                default=default,
                current=current,
                allow_none=self.allow_none,
            )

        # Skip further validation: Special case for None
        if val is None and self.allow_none:
            Diagnostics.none_value_skip_range(name)
            return None

        # Content validation
        if self._content_validator and val is not None:
            val = self._content_validator.validated(
                val,
                name,
                default=default,
                current=current,
            )

        return val

validated(value, name, current=None)

Validate through type and content validators.

Returns validated value, possibly default or current if errors occur. None may short-circuit further checks when allowed.

Source code in src/easydiffraction/core/validation.py
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
def validated(
    self,
    value,
    name,
    current=None,
):
    """Validate through type and content validators.

    Returns validated value, possibly default or current if errors
    occur. None may short-circuit further checks when allowed.
    """
    val = value
    # Evaluate callable defaults dynamically
    default = self.default() if callable(self.default) else self.default

    # Type validation
    if self._type_validator:
        val = self._type_validator.validated(
            val,
            name,
            default=default,
            current=current,
            allow_none=self.allow_none,
        )

    # Skip further validation: Special case for None
    if val is None and self.allow_none:
        Diagnostics.none_value_skip_range(name)
        return None

    # Content validation
    if self._content_validator and val is not None:
        val = self._content_validator.validated(
            val,
            name,
            default=default,
            current=current,
        )

    return val

DataTypes

Bases: Enum

Source code in src/easydiffraction/core/validation.py
28
29
30
31
32
33
34
35
36
37
38
39
40
class DataTypes(Enum):
    NUMERIC = (int, float, np.integer, np.floating, np.number)
    STRING = (str,)
    BOOL = (bool,)
    ANY = (object,)  # fallback for unconstrained

    def __str__(self):
        return self.name.lower()

    @property
    def expected_type(self):
        """Convenience alias for tuple of allowed Python types."""
        return self.value

expected_type property

Convenience alias for tuple of allowed Python types.

MembershipValidator

Bases: ValidatorBase

Ensure that a value is among allowed choices.

allowed may be an iterable or a callable returning a collection.

Source code in src/easydiffraction/core/validation.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
class MembershipValidator(ValidatorBase):
    """Ensure that a value is among allowed choices.

    `allowed` may be an iterable or a callable returning a collection.
    """

    def __init__(self, allowed):
        # Do not convert immediately to list — may be callable
        self.allowed = allowed

    def validated(
        self,
        value,
        name,
        default=None,
        current=None,
    ):
        """Validate membership and return value or fallback."""
        # Dynamically evaluate allowed if callable (e.g. lambda)
        allowed_values = self.allowed() if callable(self.allowed) else self.allowed

        if value not in allowed_values:
            Diagnostics.choice_mismatch(
                name,
                value,
                allowed_values,
                current=current,
                default=default,
            )
            return self._fallback(current, default)

        Diagnostics.validated(
            name,
            value,
            stage=ValidationStage.MEMBERSHIP,
        )
        return value

validated(value, name, default=None, current=None)

Validate membership and return value or fallback.

Source code in src/easydiffraction/core/validation.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def validated(
    self,
    value,
    name,
    default=None,
    current=None,
):
    """Validate membership and return value or fallback."""
    # Dynamically evaluate allowed if callable (e.g. lambda)
    allowed_values = self.allowed() if callable(self.allowed) else self.allowed

    if value not in allowed_values:
        Diagnostics.choice_mismatch(
            name,
            value,
            allowed_values,
            current=current,
            default=default,
        )
        return self._fallback(current, default)

    Diagnostics.validated(
        name,
        value,
        stage=ValidationStage.MEMBERSHIP,
    )
    return value

RangeValidator

Bases: ValidatorBase

Ensure a numeric value lies within [ge, le].

Source code in src/easydiffraction/core/validation.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
class RangeValidator(ValidatorBase):
    """Ensure a numeric value lies within [ge, le]."""

    def __init__(
        self,
        *,
        ge=-np.inf,
        le=np.inf,
    ):
        self.ge, self.le = ge, le

    def validated(
        self,
        value,
        name,
        default=None,
        current=None,
    ):
        """Validate range and return value or fallback."""
        if not (self.ge <= value <= self.le):
            Diagnostics.range_mismatch(
                name,
                value,
                self.ge,
                self.le,
                current=current,
                default=default,
            )
            return self._fallback(current, default)

        Diagnostics.validated(
            name,
            value,
            stage=ValidationStage.RANGE,
        )
        return value

validated(value, name, default=None, current=None)

Validate range and return value or fallback.

Source code in src/easydiffraction/core/validation.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def validated(
    self,
    value,
    name,
    default=None,
    current=None,
):
    """Validate range and return value or fallback."""
    if not (self.ge <= value <= self.le):
        Diagnostics.range_mismatch(
            name,
            value,
            self.ge,
            self.le,
            current=current,
            default=default,
        )
        return self._fallback(current, default)

    Diagnostics.validated(
        name,
        value,
        stage=ValidationStage.RANGE,
    )
    return value

RegexValidator

Bases: ValidatorBase

Ensure that a string matches a given regular expression.

Source code in src/easydiffraction/core/validation.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
class RegexValidator(ValidatorBase):
    """Ensure that a string matches a given regular expression."""

    def __init__(self, pattern):
        self.pattern = re.compile(pattern)

    def validated(
        self,
        value,
        name,
        default=None,
        current=None,
    ):
        """Validate regex and return value or fallback."""
        if not self.pattern.fullmatch(value):
            Diagnostics.regex_mismatch(
                name,
                value,
                self.pattern.pattern,
                current=current,
                default=default,
            )
            return self._fallback(current, default)

        Diagnostics.validated(
            name,
            value,
            stage=ValidationStage.REGEX,
        )
        return value

validated(value, name, default=None, current=None)

Validate regex and return value or fallback.

Source code in src/easydiffraction/core/validation.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
def validated(
    self,
    value,
    name,
    default=None,
    current=None,
):
    """Validate regex and return value or fallback."""
    if not self.pattern.fullmatch(value):
        Diagnostics.regex_mismatch(
            name,
            value,
            self.pattern.pattern,
            current=current,
            default=default,
        )
        return self._fallback(current, default)

    Diagnostics.validated(
        name,
        value,
        stage=ValidationStage.REGEX,
    )
    return value

TypeValidator

Bases: ValidatorBase

Ensure a value is of the expected Python type.

Source code in src/easydiffraction/core/validation.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
class TypeValidator(ValidatorBase):
    """Ensure a value is of the expected Python type."""

    def __init__(self, expected_type: DataTypes):
        if isinstance(expected_type, DataTypes):
            self.expected_type = expected_type
            self.expected_label = str(expected_type)
        else:
            raise TypeError(f'TypeValidator expected a DataTypes member, got {expected_type!r}')

    def validated(
        self,
        value,
        name,
        default=None,
        current=None,
        allow_none=False,
    ):
        """Validate type and return value or fallback.

        If allow_none is True, None bypasses content checks.
        """
        # Fresh initialization, use default
        if current is None and value is None:
            Diagnostics.no_value(name, default)
            return default

        # Explicit None (allowed)
        if value is None and allow_none:
            Diagnostics.none_value(name)
            return None

        # Normal type validation
        if not isinstance(value, self.expected_type.value):
            Diagnostics.type_mismatch(
                name,
                value,
                expected_type=self.expected_label,
                current=current,
                default=default,
            )
            return self._fallback(current, default)

        Diagnostics.validated(
            name,
            value,
            stage=ValidationStage.TYPE,
        )
        return value

validated(value, name, default=None, current=None, allow_none=False)

Validate type and return value or fallback.

If allow_none is True, None bypasses content checks.

Source code in src/easydiffraction/core/validation.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def validated(
    self,
    value,
    name,
    default=None,
    current=None,
    allow_none=False,
):
    """Validate type and return value or fallback.

    If allow_none is True, None bypasses content checks.
    """
    # Fresh initialization, use default
    if current is None and value is None:
        Diagnostics.no_value(name, default)
        return default

    # Explicit None (allowed)
    if value is None and allow_none:
        Diagnostics.none_value(name)
        return None

    # Normal type validation
    if not isinstance(value, self.expected_type.value):
        Diagnostics.type_mismatch(
            name,
            value,
            expected_type=self.expected_label,
            current=current,
            default=default,
        )
        return self._fallback(current, default)

    Diagnostics.validated(
        name,
        value,
        stage=ValidationStage.TYPE,
    )
    return value

ValidationStage

Bases: Enum

Phases of validation for diagnostic logging.

Source code in src/easydiffraction/core/validation.py
86
87
88
89
90
91
92
93
94
95
class ValidationStage(Enum):
    """Phases of validation for diagnostic logging."""

    TYPE = auto()
    RANGE = auto()
    MEMBERSHIP = auto()
    REGEX = auto()

    def __str__(self):
        return self.name.lower()

ValidatorBase

Bases: ABC

Abstract base class for all validators.

Source code in src/easydiffraction/core/validation.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
class ValidatorBase(ABC):
    """Abstract base class for all validators."""

    @abstractmethod
    def validated(self, value, name, default=None, current=None):
        """Return a validated value or fallback.

        Subclasses must implement this method.
        """
        raise NotImplementedError

    def _fallback(
        self,
        current=None,
        default=None,
    ):
        """Return current if set, else default."""
        return current if current is not None else default

validated(value, name, default=None, current=None) abstractmethod

Return a validated value or fallback.

Subclasses must implement this method.

Source code in src/easydiffraction/core/validation.py
106
107
108
109
110
111
112
@abstractmethod
def validated(self, value, name, default=None, current=None):
    """Return a validated value or fallback.

    Subclasses must implement this method.
    """
    raise NotImplementedError

checktype(func=None, *, context=None)

Runtime type check decorator using typeguard.

When a TypeCheckError occurs, the error is logged and None is returned. If context is provided, it is added to the message.

Source code in src/easydiffraction/core/validation.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def checktype(func=None, *, context=None):
    """Runtime type check decorator using typeguard.

    When a TypeCheckError occurs, the error is logged and None is
    returned. If context is provided, it is added to the message.
    """

    def decorator(f):
        checked_func = typechecked(f)

        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            try:
                return checked_func(*args, **kwargs)
            except TypeCheckError as err:
                msg = str(err)
                if context:
                    msg = f'{context}: {msg}'
                log.error(message=msg, exc_type=TypeError)
                return None

        return wrapper

    if func is None:
        return decorator
    return decorator(func)