Skip to content

Commit 06de6c9

Browse files
author
Steve Canny
committed
enum: transplant docx.enum.base module
Transplanted from python-pptx, with some changes to fix Python 3.3 incompatibilities. Enables full-featured enumerations to be defined declaratively. Features include setter value validation, str() value elaborated with symbolic name of value, alias names for an enumeration, mapping to and from XML enumeration (ST_*) values, and auto generation of enumeration documentation page, in addition to the base symbolic reference to an enumeration value.
1 parent e722928 commit 06de6c9

File tree

2 files changed

+447
-0
lines changed

2 files changed

+447
-0
lines changed

docx/enum/base.py

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

0 commit comments

Comments
 (0)