Skip to content

Commit 528e82e

Browse files
authored
Merge pull request #69 from OpenNTI/enum_choice
Add StrEnumChoice, a Choice that builds its vocabulary from a StrEnum
2 parents 4163586 + d487816 commit 528e82e

4 files changed

Lines changed: 161 additions & 75 deletions

File tree

CHANGES.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@
22
Changes
33
=========
44

5-
1.20.2 (unreleased)
5+
1.21.0 (unreleased)
66
===================
77

8-
- Nothing changed yet.
8+
- Add ``field.StrEnumChoice``, a schema field that uses a ``StrEnum``
9+
as its vocabulary and returns enum members (e.g., ``Color.RED``)
10+
in ``fromUnicode``.
911

1012

1113
1.20.1 (2026-06-25)
1214
===================
1315

1416
- Add some minimal typing information. In particular this makes
15-
subclasses of ``SchemaConfigured`` for type-friendly.
17+
subclasses of ``SchemaConfigured`` more type-friendly.
1618

1719

1820
1.20.0 (2026-06-01)

src/nti/schema/field.py

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# stdlib imports
1515
import numbers
1616
import re
17+
from enum import StrEnum
1718
from typing import Any
1819

1920
import zope.interface.common.idatetime
@@ -50,18 +51,19 @@
5051
from zope.schema.interfaces import IFromUnicode
5152
from zope.schema.interfaces import InvalidValue
5253

53-
from nti.schema import MessageFactory as _
54-
from nti.schema.interfaces import BeforeDictAssignedEvent
55-
from nti.schema.interfaces import BeforeObjectAssignedEvent
56-
from nti.schema.interfaces import BeforeSchemaFieldAssignedEvent
57-
from nti.schema.interfaces import BeforeSequenceAssignedEvent
58-
from nti.schema.interfaces import BeforeSetAssignedEvent
59-
from nti.schema.interfaces import BeforeTextAssignedEvent
60-
from nti.schema.interfaces import BeforeTextLineAssignedEvent
61-
from nti.schema.interfaces import IFromObject
62-
from nti.schema.interfaces import IListOrTuple
63-
from nti.schema.interfaces import IVariant
64-
from nti.schema.interfaces import VariantValidationError
54+
from . import MessageFactory as _
55+
from .interfaces import BeforeDictAssignedEvent
56+
from .interfaces import BeforeObjectAssignedEvent
57+
from .interfaces import BeforeSchemaFieldAssignedEvent
58+
from .interfaces import BeforeSequenceAssignedEvent
59+
from .interfaces import BeforeSetAssignedEvent
60+
from .interfaces import BeforeTextAssignedEvent
61+
from .interfaces import BeforeTextLineAssignedEvent
62+
from .interfaces import IFromObject
63+
from .interfaces import IListOrTuple
64+
from .interfaces import IVariant
65+
from .interfaces import IStrEnumChoice
66+
from .interfaces import VariantValidationError
6567

6668
__docformat__ = "restructuredtext en"
6769

@@ -154,10 +156,10 @@ def set(self, context, value): # pylint:disable=redefined-builtin
154156
cls.set = set
155157

156158
def __with_set(eventfactory=BeforeSchemaFieldAssignedEvent):
157-
def X(cls):
159+
def attach_set(cls):
158160
__make_set(cls, eventfactory)
159161
return cls
160-
return X
162+
return attach_set
161163

162164
def _fixup_Object_field(field, early_error=False):
163165
# TODO: Refactor and simplify.
@@ -585,9 +587,9 @@ def __init__(self, sch, min_length=0, max_length=None, **kwargs):
585587
# But to work with the superclass, we have to pass it as a keyword arg.
586588
# it's weird.
587589
super().__init__(schema=sch,
588-
min_length=min_length,
589-
max_length=max_length,
590-
**kwargs)
590+
min_length=min_length,
591+
max_length=max_length,
592+
**kwargs)
591593

592594
class Int(FieldValidationMixin, schema.Int):
593595

@@ -617,6 +619,38 @@ class Number(FieldValidationMixin, schema.Float):
617619
class ValidChoice(FieldValidationMixin, schema.Choice):
618620
pass
619621

622+
@interface.implementer(IStrEnumChoice)
623+
class StrEnumChoice(ValidChoice):
624+
"""
625+
A Choice field that takes its values from
626+
a :class:`~StrEnum`.
627+
628+
.. versionadded:: NEXT
629+
"""
630+
631+
def __init__(self, enum: type[StrEnum], **kwargs) -> None:
632+
if 'values' in kwargs or 'source' in kwargs or 'vocabulary' in kwargs:
633+
raise TypeError('Illegal keyword; values come from the enum')
634+
super().__init__(
635+
values=list(e.value for e in enum),
636+
**kwargs
637+
)
638+
self.__enum = enum
639+
640+
@property
641+
def enum(self):
642+
return self.__enum
643+
644+
def fromUnicode(self, value):
645+
# First validate the value is a member of the values
646+
# list we provided (the enum members), raising
647+
# the correct error if not.
648+
value = super().fromUnicode(value)
649+
# Then, return the actual enum member.
650+
# Subscripting the class expects the
651+
# attribute name; calling it expects the value.
652+
return self.__enum(value)
653+
620654
@__with_set()
621655
class ValidBytesLine(FieldValidationMixin, schema.BytesLine):
622656
pass

src/nti/schema/interfaces.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ def fromObject(obj):
185185
possible.
186186
"""
187187

188+
class IStrEnumChoice(sch_interfaces.IChoice):
189+
"""
190+
A choice that gets its values and validates them
191+
against a :class:`~StrEnum`.
192+
193+
.. versionadded:: NEXT
194+
"""
195+
196+
enum = Attribute("The StrEnum subclass given to the constructor.")
197+
198+
188199
class IVariant(sch_interfaces.IField, IFromObject):
189200
"""
190201
Similar to :class:`zope.schema.interfaces.IObject`, but

0 commit comments

Comments
 (0)