Skip to content

Commit 37a1551

Browse files
DirectXMan12frozencemetery
authored andcommitted
Introduce EnumExtension metaclass
This metaclass allows extensions to register new values in any enum by (ab)using metaclasses. For example, this allows for extensions to introduce new RequirementFlags without having to worry about import order.
1 parent b550c3b commit 37a1551

File tree

2 files changed

+137
-0
lines changed

2 files changed

+137
-0
lines changed

gssapi/raw/misc.pyx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ from gssapi.raw.oids cimport OID
1111

1212
from gssapi.raw.types import MechType
1313

14+
from enum import EnumMeta, Enum
15+
1416

1517
cdef extern from "python_gssapi.h":
1618
OM_uint32 gss_display_status(OM_uint32 *minor_status,
@@ -329,3 +331,64 @@ class GSSError(Exception):
329331
maj_str=maj_str,
330332
min_stat=self.min_code,
331333
min_str=min_str)
334+
335+
336+
# WARNING: FOR YOUR OWN PERSONAL SANITY, DO NOT TRY THIS AT HOME
337+
# This allows things like extensions declaring their own RequirementFlags
338+
# without having to worry about import order. There *might* be a cleaner
339+
# way to do this, but most of the ways probably exploit the interals of Enum
340+
class _EnumExtension(EnumMeta):
341+
def __new__(metacls, name, bases, classdict):
342+
# find the base class that this overrides
343+
344+
base_attr = classdict.get('__base__', None)
345+
346+
if len(bases) != 1 or bases[0] is not object or base_attr is None:
347+
raise TypeError("Enumeration extensions must be created as "
348+
"`ClassName(object)` and must contain the "
349+
"`__base__` attribute")
350+
351+
adds_to = base_attr
352+
del classdict['__base__']
353+
354+
bases = adds_to.__bases__
355+
356+
# we need to have the same Enum type
357+
if not issubclass(adds_to, Enum):
358+
raise TypeError("Enumeration extensions must extend a subtype "
359+
"of Enum or directly sublcass object")
360+
361+
# roughly preserve insertion order in Python 3.4+
362+
# (actually preserving order would require using parts of the Enum
363+
# implementation that aren't part of the spec)
364+
old_classdict = classdict
365+
classdict = metacls.__prepare__(name, bases)
366+
367+
# NB(directxman12): we can't use update, because that doesn't
368+
# trigger __setitem__, which thus won't update the _EnumDict correctly
369+
base_members = adds_to.__members__
370+
for k, v in base_members.items():
371+
classdict[k] = v.value
372+
373+
for k, v in old_classdict.items():
374+
if k in base_members:
375+
raise AttributeError("Enumeration extensions cannot override "
376+
"existing enumeration members")
377+
378+
classdict[k] = v
379+
380+
res = super(_EnumExtension, metacls).__new__(metacls, name,
381+
bases, classdict)
382+
383+
# replace the old with the new
384+
for k, v in res.__dict__.items():
385+
if (k not in ('__module__', '__qualname__', '__doc__') and
386+
k in adds_to.__dict__):
387+
setattr(adds_to, k, v)
388+
389+
# preserve enum semantics
390+
for member in adds_to.__members__.values():
391+
member.__class__ = adds_to
392+
393+
# always return the original
394+
return adds_to

gssapi/tests/test_raw.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import unittest
66

77
import should_be.all # noqa
8+
from enum import IntEnum
9+
import six
810

911
import gssapi.raw as gb
1012
import gssapi.raw.misc as gbmisc
@@ -641,6 +643,78 @@ def test_xor_set(self):
641643
fset3.should_include(gb.RequirementFlag.out_of_sequence_detection)
642644

643645

646+
class TestEnumExtension(unittest.TestCase):
647+
def setUp(self):
648+
class ExtendedEnum(IntEnum):
649+
"""some docs"""
650+
a = 1
651+
b = 2
652+
c = 3
653+
654+
class OtherEnum(IntEnum):
655+
a = 1
656+
b = 2
657+
c = 3
658+
659+
self._ext_enum = ExtendedEnum
660+
self._plain_enum = OtherEnum
661+
662+
def test_same_dict(self):
663+
@six.add_metaclass(gbmisc._EnumExtension)
664+
class ExtendedEnum(object):
665+
__base__ = self._ext_enum
666+
d = 4
667+
e = 5
668+
669+
self._ext_enum.__dict__.keys().should_be(
670+
self._plain_enum.__dict__.keys())
671+
672+
def test_members_copy_mult(self):
673+
@six.add_metaclass(gbmisc._EnumExtension)
674+
class ExtendedEnum(object):
675+
__base__ = self._ext_enum
676+
d = 4
677+
678+
@six.add_metaclass(gbmisc._EnumExtension)
679+
class ExtendedEnum2(object):
680+
__base__ = self._ext_enum
681+
e = 5
682+
683+
self._ext_enum.__members__.should_include(
684+
['a', 'b', 'c', 'd', 'e'])
685+
686+
self._ext_enum.a.should_be(1)
687+
self._ext_enum.b.should_be(2)
688+
self._ext_enum.c.should_be(3)
689+
self._ext_enum.d.should_be(4)
690+
self._ext_enum.e.should_be(5)
691+
692+
def test_all_class_are_same(self):
693+
@six.add_metaclass(gbmisc._EnumExtension)
694+
class ExtendedEnum(object):
695+
__base__ = self._ext_enum
696+
d = 4
697+
698+
assert ExtendedEnum is self._ext_enum
699+
700+
def test_members_are_still_instance(self):
701+
@six.add_metaclass(gbmisc._EnumExtension)
702+
class EnumExt(object):
703+
__base__ = self._ext_enum
704+
d = 4
705+
706+
self._ext_enum.a.should_be_a(self._ext_enum)
707+
self._ext_enum.d.should_be_a(self._ext_enum)
708+
709+
def test_doc_is_preserved(self):
710+
@six.add_metaclass(gbmisc._EnumExtension)
711+
class ExtendedEnum(object):
712+
__base__ = self._ext_enum
713+
d = 4
714+
715+
self._ext_enum.__doc__.should_be('some docs')
716+
717+
644718
class TestInitContext(_GSSAPIKerberosTestCase):
645719
def setUp(self):
646720
self.target_name = gb.import_name(TARGET_SERVICE_NAME,

0 commit comments

Comments
 (0)