|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | import enum |
6 | | -import sys |
7 | 6 | import textwrap |
8 | | -from typing import Callable, Type |
| 7 | +from typing import Any, Dict, Type, TypeVar |
9 | 8 |
|
10 | | -from docx.exceptions import InvalidXmlError |
| 9 | +from typing_extensions import Self |
11 | 10 |
|
| 11 | +_T = TypeVar("_T", bound="BaseXmlEnum") |
12 | 12 |
|
13 | | -def alias(*aliases: str) -> Callable[..., Type[enum.Enum]]: |
14 | | - """Adds alternate name for an enumeration. |
15 | 13 |
|
16 | | - Decorating a class with @alias('FOO', 'BAR', ..) allows the class to be referenced |
17 | | - by each of the names provided as arguments. |
| 14 | +class BaseEnum(int, enum.Enum): |
| 15 | + """Base class for Enums that do not map XML attr values. |
| 16 | +
|
| 17 | + The enum's value will be an integer, corresponding to the integer assigned the |
| 18 | + corresponding member in the MS API enum of the same name. |
18 | 19 | """ |
19 | 20 |
|
20 | | - def decorator(cls): |
21 | | - # alias must be set in globals from caller's frame |
22 | | - caller = sys._getframe(1) |
23 | | - globals_dict = caller.f_globals |
24 | | - for alias in aliases: |
25 | | - globals_dict[alias] = cls |
26 | | - return cls |
| 21 | + def __new__(cls, ms_api_value: int, docstr: str): |
| 22 | + self = int.__new__(cls, ms_api_value) |
| 23 | + self._value_ = ms_api_value |
| 24 | + self.__doc__ = docstr.strip() |
| 25 | + return self |
| 26 | + |
| 27 | + def __str__(self): |
| 28 | + """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'.""" |
| 29 | + return f"{self.name} ({self.value})" |
| 30 | + |
| 31 | + |
| 32 | +class BaseXmlEnum(int, enum.Enum): |
| 33 | + """Base class for Enums that also map XML attr values. |
| 34 | +
|
| 35 | + The enum's value will be an integer, corresponding to the integer assigned the |
| 36 | + corresponding member in the MS API enum of the same name. |
| 37 | + """ |
| 38 | + |
| 39 | + xml_value: str |
| 40 | + |
| 41 | + def __new__(cls, ms_api_value: int, xml_value: str, docstr: str): |
| 42 | + self = int.__new__(cls, ms_api_value) |
| 43 | + self._value_ = ms_api_value |
| 44 | + self.xml_value = xml_value |
| 45 | + self.__doc__ = docstr.strip() |
| 46 | + return self |
| 47 | + |
| 48 | + def __str__(self): |
| 49 | + """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'.""" |
| 50 | + return f"{self.name} ({self.value})" |
| 51 | + |
| 52 | + @classmethod |
| 53 | + def from_xml(cls, xml_value: str | None) -> Self: |
| 54 | + """Enumeration member corresponding to XML attribute value `xml_value`. |
| 55 | +
|
| 56 | + Example:: |
27 | 57 |
|
28 | | - return decorator |
| 58 | + >>> WD_PARAGRAPH_ALIGNMENT.from_xml("center") |
| 59 | + WD_PARAGRAPH_ALIGNMENT.CENTER |
29 | 60 |
|
| 61 | + """ |
| 62 | + member = next((member for member in cls if member.xml_value == xml_value), None) |
| 63 | + if member is None: |
| 64 | + raise ValueError(f"{cls.__name__} has no XML mapping for '{xml_value}'") |
| 65 | + return member |
30 | 66 |
|
31 | | -class _DocsPageFormatter(object): |
| 67 | + @classmethod |
| 68 | + def to_xml(cls: Type[_T], value: int | _T | None) -> str | None: |
| 69 | + """XML value of this enum member, generally an XML attribute value.""" |
| 70 | + return cls(value).xml_value |
| 71 | + |
| 72 | + |
| 73 | +class DocsPageFormatter: |
32 | 74 | """Generate an .rst doc page for an enumeration. |
33 | 75 |
|
34 | 76 | Formats a RestructuredText documention page (string) for the enumeration class parts |
35 | 77 | passed to the constructor. An immutable one-shot service object. |
36 | 78 | """ |
37 | 79 |
|
38 | | - def __init__(self, clsname, clsdict): |
| 80 | + def __init__(self, clsname: str, clsdict: Dict[str, Any]): |
39 | 81 | self._clsname = clsname |
40 | 82 | self._clsdict = clsdict |
41 | 83 |
|
@@ -67,10 +109,11 @@ def _intro_text(self): |
67 | 109 |
|
68 | 110 | return textwrap.dedent(cls_docstring).strip() |
69 | 111 |
|
70 | | - def _member_def(self, member): |
| 112 | + def _member_def(self, member: BaseEnum | BaseXmlEnum): |
71 | 113 | """Return an individual member definition formatted as an RST glossary entry, |
72 | 114 | wrapped to fit within 78 columns.""" |
73 | | - member_docstring = textwrap.dedent(member.docstring).strip() |
| 115 | + assert member.__doc__ is not None |
| 116 | + member_docstring = textwrap.dedent(member.__doc__).strip() |
74 | 117 | member_docstring = textwrap.fill( |
75 | 118 | member_docstring, |
76 | 119 | width=78, |
@@ -100,223 +143,3 @@ def _page_title(self): |
100 | 143 | double-backtics) and underlined with '=' characters.""" |
101 | 144 | title_underscore = "=" * (len(self._clsname) + 4) |
102 | 145 | return "``%s``\n%s" % (self._clsname, title_underscore) |
103 | | - |
104 | | - |
105 | | -class MetaEnumeration(type): |
106 | | - """The metaclass for Enumeration and its subclasses. |
107 | | -
|
108 | | - Adds a name for each named member and compiles state needed by the enumeration class |
109 | | - to respond to other attribute gets |
110 | | - """ |
111 | | - |
112 | | - def __new__(meta, clsname, bases, clsdict): |
113 | | - meta._add_enum_members(clsdict) |
114 | | - meta._collect_valid_settings(clsdict) |
115 | | - meta._generate_docs_page(clsname, clsdict) |
116 | | - return type.__new__(meta, clsname, bases, clsdict) |
117 | | - |
118 | | - @classmethod |
119 | | - def _add_enum_members(meta, clsdict): |
120 | | - """Dispatch ``.add_to_enum()`` call to each member so it can do its thing to |
121 | | - properly add itself to the enumeration class. |
122 | | -
|
123 | | - This delegation allows member sub-classes to add specialized behaviors. |
124 | | - """ |
125 | | - enum_members = clsdict["__members__"] |
126 | | - for member in enum_members: |
127 | | - member.add_to_enum(clsdict) |
128 | | - |
129 | | - @classmethod |
130 | | - def _collect_valid_settings(meta, clsdict): |
131 | | - """Return a sequence containing the enumeration values that are valid assignment |
132 | | - values. |
133 | | -
|
134 | | - Return-only values are excluded. |
135 | | - """ |
136 | | - enum_members = clsdict["__members__"] |
137 | | - valid_settings = [] |
138 | | - for member in enum_members: |
139 | | - valid_settings.extend(member.valid_settings) |
140 | | - clsdict["_valid_settings"] = valid_settings |
141 | | - |
142 | | - @classmethod |
143 | | - def _generate_docs_page(meta, clsname, clsdict): |
144 | | - """Return the RST documentation page for the enumeration.""" |
145 | | - clsdict["__docs_rst__"] = _DocsPageFormatter(clsname, clsdict).page_str |
146 | | - |
147 | | - |
148 | | -class EnumerationBase(object): |
149 | | - """Base class for all enumerations, used directly for enumerations requiring only |
150 | | - basic behavior. |
151 | | -
|
152 | | - It's __dict__ is used below in the Python 2+3 compatible metaclass definition. |
153 | | - """ |
154 | | - |
155 | | - __members__ = () |
156 | | - __ms_name__ = "" |
157 | | - |
158 | | - @classmethod |
159 | | - def validate(cls, value): |
160 | | - """Raise |ValueError| if `value` is not an assignable value.""" |
161 | | - if value not in cls._valid_settings: |
162 | | - raise ValueError( |
163 | | - "%s not a member of %s enumeration" % (value, cls.__name__) |
164 | | - ) |
165 | | - |
166 | | - |
167 | | -Enumeration = MetaEnumeration("Enumeration", (object,), dict(EnumerationBase.__dict__)) |
168 | | - |
169 | | - |
170 | | -class XmlEnumeration(Enumeration): |
171 | | - """Provides ``to_xml()`` and ``from_xml()`` methods in addition to base enumeration |
172 | | - features.""" |
173 | | - |
174 | | - __members__ = () |
175 | | - __ms_name__ = "" |
176 | | - |
177 | | - @classmethod |
178 | | - def from_xml(cls, xml_val): |
179 | | - """Return the enumeration member corresponding to the XML value `xml_val`.""" |
180 | | - if xml_val not in cls._xml_to_member: |
181 | | - raise InvalidXmlError( |
182 | | - "attribute value '%s' not valid for this type" % xml_val |
183 | | - ) |
184 | | - return cls._xml_to_member[xml_val] |
185 | | - |
186 | | - @classmethod |
187 | | - def to_xml(cls, enum_val): |
188 | | - """Return the XML value of the enumeration value `enum_val`.""" |
189 | | - if enum_val not in cls._member_to_xml: |
190 | | - raise ValueError( |
191 | | - "value '%s' not in enumeration %s" % (enum_val, cls.__name__) |
192 | | - ) |
193 | | - return cls._member_to_xml[enum_val] |
194 | | - |
195 | | - |
196 | | -class EnumMember(object): |
197 | | - """Used in the enumeration class definition to define a member value and its |
198 | | - mappings.""" |
199 | | - |
200 | | - def __init__(self, name, value, docstring): |
201 | | - self._name = name |
202 | | - if isinstance(value, int): |
203 | | - value = EnumValue(name, value, docstring) |
204 | | - self._value = value |
205 | | - self._docstring = docstring |
206 | | - |
207 | | - def add_to_enum(self, clsdict): |
208 | | - """Add a name to `clsdict` for this member.""" |
209 | | - self.register_name(clsdict) |
210 | | - |
211 | | - @property |
212 | | - def docstring(self): |
213 | | - """The description of this member.""" |
214 | | - return self._docstring |
215 | | - |
216 | | - @property |
217 | | - def name(self): |
218 | | - """The distinguishing name of this member within the enumeration class, e.g. |
219 | | - 'MIDDLE' for MSO_VERTICAL_ANCHOR.MIDDLE, if this is a named member. |
220 | | -
|
221 | | - Otherwise the primitive value such as |None|, |True| or |False|. |
222 | | - """ |
223 | | - return self._name |
224 | | - |
225 | | - def register_name(self, clsdict): |
226 | | - """Add a member name to the class dict `clsdict` containing the value of this |
227 | | - member object. |
228 | | -
|
229 | | - Where the name of this object is None, do nothing; this allows out-of-band |
230 | | - values to be defined without adding a name to the class dict. |
231 | | - """ |
232 | | - if self.name is None: |
233 | | - return |
234 | | - clsdict[self.name] = self.value |
235 | | - |
236 | | - @property |
237 | | - def valid_settings(self): |
238 | | - """A sequence containing the values valid for assignment for this member. |
239 | | -
|
240 | | - May be zero, one, or more in number. |
241 | | - """ |
242 | | - return (self._value,) |
243 | | - |
244 | | - @property |
245 | | - def value(self): |
246 | | - """The enumeration value for this member, often an instance of EnumValue, but |
247 | | - may be a primitive value such as |None|.""" |
248 | | - return self._value |
249 | | - |
250 | | - |
251 | | -class EnumValue(int): |
252 | | - """A named enumeration value, providing __str__ and __doc__ string values for its |
253 | | - symbolic name and description, respectively. |
254 | | -
|
255 | | - Subclasses int, so behaves as a regular int unless the strings are asked for. |
256 | | - """ |
257 | | - |
258 | | - def __new__(cls, member_name, int_value, docstring): |
259 | | - return super(EnumValue, cls).__new__(cls, int_value) |
260 | | - |
261 | | - def __init__(self, member_name, int_value, docstring): |
262 | | - super(EnumValue, self).__init__() |
263 | | - self._member_name = member_name |
264 | | - self._docstring = docstring |
265 | | - |
266 | | - @property |
267 | | - def __doc__(self): |
268 | | - """The description of this enumeration member.""" |
269 | | - return self._docstring.strip() |
270 | | - |
271 | | - def __str__(self): |
272 | | - """The symbolic name and string value of this member, e.g. 'MIDDLE (3)'.""" |
273 | | - return "%s (%d)" % (self._member_name, int(self)) |
274 | | - |
275 | | - |
276 | | -class ReturnValueOnlyEnumMember(EnumMember): |
277 | | - """Used to define a member of an enumeration that is only valid as a query result |
278 | | - and is not valid as a setting, e.g. MSO_VERTICAL_ANCHOR.MIXED (-2)""" |
279 | | - |
280 | | - @property |
281 | | - def valid_settings(self): |
282 | | - """No settings are valid for a return-only value.""" |
283 | | - return () |
284 | | - |
285 | | - |
286 | | -class XmlMappedEnumMember(EnumMember): |
287 | | - """Used to define a member whose value maps to an XML attribute value.""" |
288 | | - |
289 | | - def __init__(self, name, value, xml_value, docstring): |
290 | | - super(XmlMappedEnumMember, self).__init__(name, value, docstring) |
291 | | - self._xml_value = xml_value |
292 | | - |
293 | | - def add_to_enum(self, clsdict): |
294 | | - """Compile XML mappings in addition to base add behavior.""" |
295 | | - super(XmlMappedEnumMember, self).add_to_enum(clsdict) |
296 | | - self.register_xml_mapping(clsdict) |
297 | | - |
298 | | - def register_xml_mapping(self, clsdict): |
299 | | - """Add XML mappings to the enumeration class state for this member.""" |
300 | | - member_to_xml = self._get_or_add_member_to_xml(clsdict) |
301 | | - member_to_xml[self.value] = self.xml_value |
302 | | - xml_to_member = self._get_or_add_xml_to_member(clsdict) |
303 | | - xml_to_member[self.xml_value] = self.value |
304 | | - |
305 | | - @property |
306 | | - def xml_value(self): |
307 | | - """The XML attribute value that corresponds to this enumeration value.""" |
308 | | - return self._xml_value |
309 | | - |
310 | | - @staticmethod |
311 | | - def _get_or_add_member_to_xml(clsdict): |
312 | | - """Add the enum -> xml value mapping to the enumeration class state.""" |
313 | | - if "_member_to_xml" not in clsdict: |
314 | | - clsdict["_member_to_xml"] = {} |
315 | | - return clsdict["_member_to_xml"] |
316 | | - |
317 | | - @staticmethod |
318 | | - def _get_or_add_xml_to_member(clsdict): |
319 | | - """Add the xml -> enum value mapping to the enumeration class state.""" |
320 | | - if "_xml_to_member" not in clsdict: |
321 | | - clsdict["_xml_to_member"] = {} |
322 | | - return clsdict["_xml_to_member"] |
0 commit comments