Skip to content

Commit beb6f79

Browse files
author
Deepak Mewar
committed
Added functionality for multiple aliases as a list
1 parent b7d6dbe commit beb6f79

3 files changed

Lines changed: 206 additions & 193 deletions

File tree

‎docs/source/identifiers.rst‎

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ representations:
2020
1. An index (integer)
2121
2. A name (short string)
2222
3. A description (long string)
23-
4. An alias (optional short string)
23+
4. An alias (optional short string or list of short strings)
2424

2525

2626
Identifiers in IMAS-Python
@@ -51,7 +51,6 @@ the available identifiers is stored as ``imas.identifiers.identifiers``.
5151
mid = imas.identifiers.materials_identifier
5252
print(mid["235U"]) # Access by canonical name
5353
print(mid["U_235"]) # Access by alias
54-
print(mid["235U"].alias) # Print the alias
5554
5655
# Both return the same object
5756
assert mid["235U"] is mid["U_235"]
@@ -98,7 +97,7 @@ below example:
9897
# 3. Assign an integer. This looks up the index in the identifier enum:
9998
core_sources.source[0].identifier = 1
10099
101-
# For identifiers with aliases, you can also assign using the alias:
100+
# Identifiers can still be assigned with the old alias name for backward compatibility:
102101
materials = imas.IDSFactory().materials()
103102
materials.material.resize(1)
104103
mid = imas.identifiers.materials_identifier
@@ -125,20 +124,18 @@ below example:
125124
Identifier aliases
126125
------------------
127126

128-
Some identifiers may have aliases defined in the Data Dictionary. An alias provides
129-
an alternative name for accessing the same identifier value. This is particularly
130-
useful where multiple naming conventions exist.
127+
Some identifiers may have multiple aliases defined in the Data Dictionary. Aliases are
128+
former names kept as an option to ensure better backward compatibility after a change
129+
and support multiple naming conventions. An identifier can have any number of
130+
comma-separated aliases.
131131

132-
Aliases can be accessed in the same ways as canonical names.
133-
134-
Aliases starting with a number can be accessible using ["235U"] but not
135-
material_identifier.235U because of Python construct limitation.
132+
Aliases can be accessed in the same ways as canonical names, and all aliases for an
133+
identifier point to the same object.
136134

137135
Aliases that begin with a number (e.g., 235U) cannot be accessed using dot notation
138-
(e.g., material_identifier.235U) due to Python’s syntax restrictions. Instead, such
139-
aliases must be accessed using dictionary-style indexing,
140-
for example: material_identifier["235U"].
141-
136+
(e.g., material_identifier.235U) due to Python's syntax restrictions. Instead, such
137+
aliases must be accessed using dictionary-style indexing, for example:
138+
material_identifier["235U"].
142139

143140
.. code-block:: python
144141
:caption: Working with identifier aliases
@@ -151,27 +148,30 @@ for example: material_identifier["235U"].
151148
# Access by canonical name
152149
uranium235_by_name = mid["235U"]
153150
print(f"Name: {uranium235_by_name.name}")
154-
print(f"Alias: {uranium235_by_name.alias}")
151+
print(f"Aliases: {uranium235_by_name.aliases}") # List of all aliases
152+
print(f"First alias: {uranium235_by_name.alias}") # First alias for compatibility
155153
print(f"Index: {uranium235_by_name.index}")
156154
print(f"Description: {uranium235_by_name.description}")
157155
158-
# Access by alias - returns the same object
159-
uranium235_by_alias = mid["U_235"]
160-
print(f"Same object: {uranium235_by_name is uranium235_by_alias}")
156+
# Access by any alias - all return the same object
157+
uranium235_by_alias1 = mid["U_235"]
158+
uranium235_by_alias2 = mid["Uranium_235"]
159+
print(f"Same objects: {uranium235_by_name is uranium235_by_alias1 is uranium235_by_alias2}")
161160
162161
# You can also use attribute access for aliases (when valid Python identifiers)
163162
uranium235_by_attr = mid.U_235
164163
print(f"Same object: {uranium235_by_name is uranium235_by_attr}")
165164
166-
# When assigning to IDS structures, aliases work the same way
165+
# When assigning to IDS structures, any alias works the same way
167166
materials = imas.IDSFactory().materials()
168167
materials.material.resize(1)
169168
170-
# These assignments are equivalent:
171-
materials.material[0].identifier = "235U" # canonical name
172-
materials.material[0].identifier = "U_235" # alias
173-
materials.material[0].identifier = mid["235U"] # enum value
174-
materials.material[0].identifier = mid.U_235 # enum value via alias
169+
# These assignments are all equivalent:
170+
materials.material[0].identifier = "235U" # canonical name
171+
materials.material[0].identifier = "U_235" # first alias
172+
materials.material[0].identifier = "Uranium_235" # another alias
173+
materials.material[0].identifier = mid["235U"] # enum value
174+
materials.material[0].identifier = mid.U_235 # enum value via alias
175175
176176
177177
Compare identifiers

‎imas/ids_identifiers.py‎

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# This file is part of IMAS-Python.
22
# You should have received the IMAS-Python LICENSE file with this project.
3-
"""IMAS-Python module to support Data Dictionary identifiers.
4-
"""
3+
"""IMAS-Python module to support Data Dictionary identifiers."""
54

65
import logging
76
from enum import Enum, EnumMeta
@@ -30,20 +29,34 @@ def __getitem__(cls, name):
3029

3130

3231
class IDSIdentifier(Enum, metaclass=IDSIdentifierMeta):
32+
# class IDSIdentifier(Enum):
3333
"""Base class for all identifier enums."""
3434

35-
def __new__(self, value: int, description: str, alias: str = None):
36-
obj = object.__new__(self)
35+
def __new__(cls, value: int, description: str, aliases=None):
36+
obj = object.__new__(cls)
3737
obj._value_ = value
3838
return obj
3939

40-
def __init__(self, value: int, description: str, alias: str = None) -> None:
40+
def __init__(self, value: int, description: str, aliases=None) -> None:
4141
self.index = value
4242
"""Unique index for this identifier value."""
4343
self.description = description
4444
"""Description for this identifier value."""
45-
self.alias = alias
46-
"""Alias for this identifier value (if any)."""
45+
46+
# Store aliases as a list for multi-alias support; keep a single 'alias'
47+
# attribute pointing to the first alias for backwards compatibility.
48+
if aliases is None:
49+
self.aliases = []
50+
else:
51+
# Accept either a single string or list of strings
52+
if isinstance(aliases, str):
53+
# comma-separated string -> list
54+
parsed = [a.strip() for a in aliases.split(",") if a.strip()]
55+
else:
56+
parsed = list(aliases)
57+
self.aliases = parsed
58+
if self.aliases:
59+
self.alias = self.aliases[0]
4760

4861
def __eq__(self, other):
4962
if self is other:
@@ -52,37 +65,33 @@ def __eq__(self, other):
5265
other_name = str(other.name)
5366
other_index = int(other.index)
5467
other_description = str(other.description)
55-
other_alias = getattr(other, "alias", None)
5668
except (AttributeError, TypeError, ValueError):
5769
# Attribute doesn't exist, or failed to convert
5870
return NotImplemented
71+
5972
# Index must match
6073
if other_index == self.index:
6174
# Name may be left empty, or match name or alias
62-
name_matches = (
75+
if (
6376
other_name == self.name
6477
or other_name == ""
65-
or (self.alias and other_name == self.alias)
66-
or (other_alias and other_alias == self.name)
67-
or (self.alias and other_alias and self.alias == other_alias)
68-
)
69-
if name_matches:
78+
or other_name in getattr(self, "aliases", [])
79+
):
7080
# Description doesn't have to match, though we will warn when it doesn't
71-
if other_description != self.description and other_description != "":
81+
if other_description not in (self.description, ""):
7282
logger.warning(
7383
"Description of %r does not match identifier description %r",
7484
other.description,
7585
self.description,
7686
)
7787
return True
78-
else:
79-
logger.warning(
80-
"Name %r does not match identifier name %r or "
81-
"alias %r, but indexes are equal.",
82-
other.name,
83-
self.name,
84-
self.alias,
85-
)
88+
89+
# If we get here with matching indexes but no name/alias match, warn
90+
logger.warning(
91+
"Name %r does not match identifier name %r, but indexes are equal.",
92+
other.name,
93+
self.name,
94+
)
8695
return False
8796

8897
@classmethod
@@ -92,16 +101,21 @@ def _from_xml(cls, identifier_name, xml) -> Type["IDSIdentifier"]:
92101
aliases = {}
93102
for int_element in element.iterfind("int"):
94103
name = int_element.get("name")
95-
alias = (
96-
int_element.get("alias")
97-
if int_element.get("alias") is not None
98-
else None
99-
)
100104
value = int_element.text
101105
description = int_element.get("description")
102-
enum_values[name] = (int(value), description, alias)
103-
if alias:
104-
aliases[alias] = name
106+
# alias attribute may contain multiple comma-separated aliases
107+
alias_attr = int_element.get("alias")
108+
if alias_attr:
109+
aliases = [a.strip() for a in alias_attr.split(",") if a.strip()]
110+
else:
111+
aliases = []
112+
# Canonical entry: use the canonical 'name' as key
113+
enum_values[name] = (int(value), description, aliases)
114+
# Also add alias names as enum *aliases* (they become enum attributes)
115+
# only if they are valid Python identifiers and not already present.
116+
for match in aliases:
117+
if match and match not in enum_values:
118+
enum_values[match] = (int(value), description, aliases)
105119
# Create the enumeration
106120
enum = cls(
107121
identifier_name,
@@ -110,9 +124,6 @@ def _from_xml(cls, identifier_name, xml) -> Type["IDSIdentifier"]:
110124
qualname=f"{__name__}.{identifier_name}",
111125
)
112126
enum.__doc__ = element.find("header").text
113-
enum._aliases = aliases
114-
for alias, canonical_name in aliases.items():
115-
setattr(enum, alias, getattr(enum, canonical_name))
116127
return enum
117128

118129

0 commit comments

Comments
 (0)