Skip to content

Commit 4f6fa5b

Browse files
authored
Merge pull request #193 from CitrineInformatics/feature/migrate-enum
Add migration support to BaseEnumeration
2 parents ad7e5fe + 70d3be6 commit 4f6fa5b

File tree

3 files changed

+109
-7
lines changed

3 files changed

+109
-7
lines changed

gemd/enumeration/base_enumeration.py

+68-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Base class for all enumerations."""
22
from deprecation import deprecated
33
from enum import Enum
4-
from typing import Optional
4+
from typing import Optional, Type, Callable
5+
from warnings import warn
56

67

78
class BaseEnumeration(str, Enum):
@@ -56,10 +57,6 @@ def from_str(cls, val: str, *, exception: bool = False) -> Optional["BaseEnumera
5657
BaseEnumeration
5758
The matching enumerated element, or None
5859
59-
:param val:
60-
:param exception:
61-
:return:
62-
6360
"""
6461
if val is None:
6562
result = None
@@ -102,3 +99,69 @@ def get_enum(cls, name: str) -> "BaseEnumeration":
10299
def __str__(self):
103100
"""Return the value of the enumeration object."""
104101
return self.value
102+
103+
@classmethod
104+
def _missing_(cls, value: object) -> Optional["BaseEnumeration"]:
105+
"""Allow Class(value) to resolve synonyms."""
106+
if isinstance(value, str):
107+
return cls.from_str(value)
108+
else:
109+
return None
110+
111+
112+
def migrated_enum(*,
113+
old_value: str,
114+
new_value: str,
115+
deprecated_in: str,
116+
removed_in: str) -> Callable[[Type], Type]:
117+
"""
118+
Decorator for registering an enumerated value as migrated to a new symbol.
119+
120+
Parameters
121+
----------
122+
old_value: str
123+
A string containing the old symbol name. Used for display only.
124+
new_value: str
125+
A string containing the new symbol name or the enumeration value. Used
126+
to resolve the target value.
127+
deprecated_in: str
128+
The version of the library the enumerated value was migrated.
129+
removed_in: str
130+
The version of the library the old enumerated value will be removed in.
131+
132+
"""
133+
def decorator(cls) -> Type:
134+
print("Sear")
135+
136+
class MixinMeta(type(cls)):
137+
"""New derived metaclass for holding the deprecated symbol."""
138+
139+
def __getitem__(cls, name):
140+
if name == old_value:
141+
warn(
142+
f"{old_value} is deprecated as of {deprecated_in} "
143+
f"and will be removed in {removed_in}. "
144+
f"{old_value} has been renamed to {cls(new_value).name}.",
145+
DeprecationWarning
146+
)
147+
return cls(new_value)
148+
else:
149+
return super().__getitem__(name)
150+
151+
def accessor(self):
152+
"""Subroutine that returns the new enumerated value."""
153+
return cls(new_value)
154+
155+
accessor.__name__ = old_value # So deprecated knows the correct target name
156+
deprecator = deprecated(deprecated_in=deprecated_in,
157+
removed_in=removed_in,
158+
details=f"{old_value} has been renamed to {cls(new_value).name}.",
159+
)
160+
161+
# Add the property to the metaclass, and then update cls' meta
162+
setattr(MixinMeta, old_value, property(deprecator(accessor)))
163+
cls.__class__ = MixinMeta
164+
165+
return cls
166+
167+
return decorator

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
packages.append("")
55

66
setup(name='gemd',
7-
version='1.15.0',
7+
version='1.16.0',
88
python_requires='>=3.7',
99
url='http://github.com/CitrineInformatics/gemd-python',
1010
description="Python binding for Citrine's GEMD data model",

tests/enumeration/test_enumeration.py

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Tests of the enumeration class."""
22
import pytest
3+
import warnings
34

45
from gemd.entity.attribute.property import Property
56
from gemd.enumeration import Origin
6-
from gemd.enumeration.base_enumeration import BaseEnumeration
7+
from gemd.enumeration.base_enumeration import BaseEnumeration, migrated_enum
78
from gemd.json import loads, dumps
89

910

@@ -81,3 +82,41 @@ class TestEnum(BaseEnumeration):
8182
assert (
8283
TestEnum.from_str(key.upper()) == TestEnum.TWO
8384
), f"from_str didn't resolve {key.upper()}"
85+
86+
87+
def test_missing():
88+
"""Test that enumeration is resolved via multiple paths."""
89+
class TestEnum(BaseEnumeration):
90+
ONE = "One", "1"
91+
TWO = "Two", "2"
92+
93+
assert TestEnum("One") == TestEnum.ONE
94+
assert TestEnum("ONE") == TestEnum.ONE
95+
assert TestEnum(TestEnum.ONE) == TestEnum.ONE
96+
assert TestEnum("1") == TestEnum.ONE
97+
98+
with pytest.raises(ValueError):
99+
TestEnum("Uno")
100+
101+
with pytest.raises(ValueError):
102+
TestEnum(1)
103+
104+
105+
def test_migrated():
106+
"""Verify that migration functions as expected."""
107+
@migrated_enum(old_value="UNO", new_value="ONE", deprecated_in="1.9.9", removed_in="2.0.0")
108+
class TestEnum(BaseEnumeration):
109+
ONE = "One", "1"
110+
TWO = "Two", "2"
111+
112+
assert TestEnum.ONE == "One"
113+
114+
with pytest.deprecated_call(match=r"ONE"):
115+
assert TestEnum.UNO == "One"
116+
117+
with pytest.deprecated_call(match=r"ONE"):
118+
assert TestEnum["UNO"] == "One"
119+
120+
with warnings.catch_warnings():
121+
warnings.simplefilter("error")
122+
assert TestEnum["ONE"] == "One"

0 commit comments

Comments
 (0)