365 lines
10 KiB
Python
365 lines
10 KiB
Python
|
# encoding: utf-8
|
||
|
|
||
|
"""
|
||
|
Base classes and other objects used by enumerations
|
||
|
"""
|
||
|
|
||
|
from __future__ import absolute_import, print_function
|
||
|
|
||
|
import sys
|
||
|
import textwrap
|
||
|
|
||
|
|
||
|
def alias(*aliases):
|
||
|
"""
|
||
|
Decorating a class with @alias('FOO', 'BAR', ..) allows the class to
|
||
|
be referenced by each of the names provided as arguments.
|
||
|
"""
|
||
|
|
||
|
def decorator(cls):
|
||
|
# alias must be set in globals from caller's frame
|
||
|
caller = sys._getframe(1)
|
||
|
globals_dict = caller.f_globals
|
||
|
for alias in aliases:
|
||
|
globals_dict[alias] = cls
|
||
|
return cls
|
||
|
|
||
|
return decorator
|
||
|
|
||
|
|
||
|
class _DocsPageFormatter(object):
|
||
|
"""
|
||
|
Formats a RestructuredText documention page (string) for the enumeration
|
||
|
class parts passed to the constructor. An immutable one-shot service
|
||
|
object.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, clsname, clsdict):
|
||
|
self._clsname = clsname
|
||
|
self._clsdict = clsdict
|
||
|
|
||
|
@property
|
||
|
def page_str(self):
|
||
|
"""
|
||
|
The RestructuredText documentation page for the enumeration. This is
|
||
|
the only API member for the class.
|
||
|
"""
|
||
|
tmpl = ".. _%s:\n\n%s\n\n%s\n\n----\n\n%s"
|
||
|
components = (
|
||
|
self._ms_name,
|
||
|
self._page_title,
|
||
|
self._intro_text,
|
||
|
self._member_defs,
|
||
|
)
|
||
|
return tmpl % components
|
||
|
|
||
|
@property
|
||
|
def _intro_text(self):
|
||
|
"""
|
||
|
The docstring of the enumeration, formatted for use at the top of the
|
||
|
documentation page
|
||
|
"""
|
||
|
try:
|
||
|
cls_docstring = self._clsdict["__doc__"]
|
||
|
except KeyError:
|
||
|
cls_docstring = ""
|
||
|
|
||
|
if cls_docstring is None:
|
||
|
return ""
|
||
|
|
||
|
return textwrap.dedent(cls_docstring).strip()
|
||
|
|
||
|
def _member_def(self, member):
|
||
|
"""
|
||
|
Return an individual member definition formatted as an RST glossary
|
||
|
entry, wrapped to fit within 78 columns.
|
||
|
"""
|
||
|
member_docstring = textwrap.dedent(member.docstring).strip()
|
||
|
member_docstring = textwrap.fill(
|
||
|
member_docstring,
|
||
|
width=78,
|
||
|
initial_indent=" " * 4,
|
||
|
subsequent_indent=" " * 4,
|
||
|
)
|
||
|
return "%s\n%s\n" % (member.name, member_docstring)
|
||
|
|
||
|
@property
|
||
|
def _member_defs(self):
|
||
|
"""
|
||
|
A single string containing the aggregated member definitions section
|
||
|
of the documentation page
|
||
|
"""
|
||
|
members = self._clsdict["__members__"]
|
||
|
member_defs = [
|
||
|
self._member_def(member) for member in members if member.name is not None
|
||
|
]
|
||
|
return "\n".join(member_defs)
|
||
|
|
||
|
@property
|
||
|
def _ms_name(self):
|
||
|
"""
|
||
|
The Microsoft API name for this enumeration
|
||
|
"""
|
||
|
return self._clsdict["__ms_name__"]
|
||
|
|
||
|
@property
|
||
|
def _page_title(self):
|
||
|
"""
|
||
|
The title for the documentation page, formatted as code (surrounded
|
||
|
in double-backtics) and underlined with '=' characters
|
||
|
"""
|
||
|
title_underscore = "=" * (len(self._clsname) + 4)
|
||
|
return "``%s``\n%s" % (self._clsname, title_underscore)
|
||
|
|
||
|
|
||
|
class MetaEnumeration(type):
|
||
|
"""
|
||
|
The metaclass for Enumeration and its subclasses. Adds a name for each
|
||
|
named member and compiles state needed by the enumeration class to
|
||
|
respond to other attribute gets
|
||
|
"""
|
||
|
|
||
|
def __new__(meta, clsname, bases, clsdict):
|
||
|
meta._add_enum_members(clsdict)
|
||
|
meta._collect_valid_settings(clsdict)
|
||
|
meta._generate_docs_page(clsname, clsdict)
|
||
|
return type.__new__(meta, clsname, bases, clsdict)
|
||
|
|
||
|
@classmethod
|
||
|
def _add_enum_members(meta, clsdict):
|
||
|
"""
|
||
|
Dispatch ``.add_to_enum()`` call to each member so it can do its
|
||
|
thing to properly add itself to the enumeration class. This
|
||
|
delegation allows member sub-classes to add specialized behaviors.
|
||
|
"""
|
||
|
enum_members = clsdict["__members__"]
|
||
|
for member in enum_members:
|
||
|
member.add_to_enum(clsdict)
|
||
|
|
||
|
@classmethod
|
||
|
def _collect_valid_settings(meta, clsdict):
|
||
|
"""
|
||
|
Return a sequence containing the enumeration values that are valid
|
||
|
assignment values. Return-only values are excluded.
|
||
|
"""
|
||
|
enum_members = clsdict["__members__"]
|
||
|
valid_settings = []
|
||
|
for member in enum_members:
|
||
|
valid_settings.extend(member.valid_settings)
|
||
|
clsdict["_valid_settings"] = valid_settings
|
||
|
|
||
|
@classmethod
|
||
|
def _generate_docs_page(meta, clsname, clsdict):
|
||
|
"""
|
||
|
Return the RST documentation page for the enumeration.
|
||
|
"""
|
||
|
clsdict["__docs_rst__"] = _DocsPageFormatter(clsname, clsdict).page_str
|
||
|
|
||
|
|
||
|
class EnumerationBase(object):
|
||
|
"""
|
||
|
Base class for all enumerations, used directly for enumerations requiring
|
||
|
only basic behavior. It's __dict__ is used below in the Python 2+3
|
||
|
compatible metaclass definition.
|
||
|
"""
|
||
|
|
||
|
__members__ = ()
|
||
|
__ms_name__ = ""
|
||
|
|
||
|
@classmethod
|
||
|
def validate(cls, value):
|
||
|
"""
|
||
|
Raise |ValueError| if *value* is not an assignable value.
|
||
|
"""
|
||
|
if value not in cls._valid_settings:
|
||
|
raise ValueError(
|
||
|
"%s not a member of %s enumeration" % (value, cls.__name__)
|
||
|
)
|
||
|
|
||
|
|
||
|
Enumeration = MetaEnumeration("Enumeration", (object,), dict(EnumerationBase.__dict__))
|
||
|
|
||
|
|
||
|
class XmlEnumeration(Enumeration):
|
||
|
"""
|
||
|
Provides ``to_xml()`` and ``from_xml()`` methods in addition to base
|
||
|
enumeration features
|
||
|
"""
|
||
|
|
||
|
__members__ = ()
|
||
|
__ms_name__ = ""
|
||
|
|
||
|
@classmethod
|
||
|
def from_xml(cls, xml_val):
|
||
|
"""
|
||
|
Return the enumeration member corresponding to the XML value
|
||
|
*xml_val*.
|
||
|
"""
|
||
|
return cls._xml_to_member[xml_val]
|
||
|
|
||
|
@classmethod
|
||
|
def to_xml(cls, enum_val):
|
||
|
"""
|
||
|
Return the XML value of the enumeration value *enum_val*.
|
||
|
"""
|
||
|
cls.validate(enum_val)
|
||
|
return cls._member_to_xml[enum_val]
|
||
|
|
||
|
|
||
|
class EnumMember(object):
|
||
|
"""
|
||
|
Used in the enumeration class definition to define a member value and its
|
||
|
mappings
|
||
|
"""
|
||
|
|
||
|
def __init__(self, name, value, docstring):
|
||
|
self._name = name
|
||
|
if isinstance(value, int):
|
||
|
value = EnumValue(name, value, docstring)
|
||
|
self._value = value
|
||
|
self._docstring = docstring
|
||
|
|
||
|
def add_to_enum(self, clsdict):
|
||
|
"""
|
||
|
Add a name to *clsdict* for this member.
|
||
|
"""
|
||
|
self.register_name(clsdict)
|
||
|
|
||
|
@property
|
||
|
def docstring(self):
|
||
|
"""
|
||
|
The description of this member
|
||
|
"""
|
||
|
return self._docstring
|
||
|
|
||
|
@property
|
||
|
def name(self):
|
||
|
"""
|
||
|
The distinguishing name of this member within the enumeration class,
|
||
|
e.g. 'MIDDLE' for MSO_VERTICAL_ANCHOR.MIDDLE, if this is a named
|
||
|
member. Otherwise the primitive value such as |None|, |True| or
|
||
|
|False|.
|
||
|
"""
|
||
|
return self._name
|
||
|
|
||
|
def register_name(self, clsdict):
|
||
|
"""
|
||
|
Add a member name to the class dict *clsdict* containing the value of
|
||
|
this member object. Where the name of this object is None, do
|
||
|
nothing; this allows out-of-band values to be defined without adding
|
||
|
a name to the class dict.
|
||
|
"""
|
||
|
if self.name is None:
|
||
|
return
|
||
|
clsdict[self.name] = self.value
|
||
|
|
||
|
@property
|
||
|
def valid_settings(self):
|
||
|
"""
|
||
|
A sequence containing the values valid for assignment for this
|
||
|
member. May be zero, one, or more in number.
|
||
|
"""
|
||
|
return (self._value,)
|
||
|
|
||
|
@property
|
||
|
def value(self):
|
||
|
"""
|
||
|
The enumeration value for this member, often an instance of
|
||
|
EnumValue, but may be a primitive value such as |None|.
|
||
|
"""
|
||
|
return self._value
|
||
|
|
||
|
|
||
|
class EnumValue(int):
|
||
|
"""
|
||
|
A named enumeration value, providing __str__ and __doc__ string values
|
||
|
for its symbolic name and description, respectively. Subclasses int, so
|
||
|
behaves as a regular int unless the strings are asked for.
|
||
|
"""
|
||
|
|
||
|
def __new__(cls, member_name, int_value, docstring):
|
||
|
return super(EnumValue, cls).__new__(cls, int_value)
|
||
|
|
||
|
def __init__(self, member_name, int_value, docstring):
|
||
|
super(EnumValue, self).__init__()
|
||
|
self._member_name = member_name
|
||
|
self._docstring = docstring
|
||
|
|
||
|
@property
|
||
|
def __doc__(self):
|
||
|
"""
|
||
|
The description of this enumeration member
|
||
|
"""
|
||
|
return self._docstring.strip()
|
||
|
|
||
|
def __str__(self):
|
||
|
"""
|
||
|
The symbolic name and string value of this member, e.g. 'MIDDLE (3)'
|
||
|
"""
|
||
|
return "{0:s} ({1:d})".format(self._member_name, self)
|
||
|
|
||
|
|
||
|
class ReturnValueOnlyEnumMember(EnumMember):
|
||
|
"""
|
||
|
Used to define a member of an enumeration that is only valid as a query
|
||
|
result and is not valid as a setting, e.g. MSO_VERTICAL_ANCHOR.MIXED (-2)
|
||
|
"""
|
||
|
|
||
|
@property
|
||
|
def valid_settings(self):
|
||
|
"""
|
||
|
No settings are valid for a return-only value.
|
||
|
"""
|
||
|
return ()
|
||
|
|
||
|
|
||
|
class XmlMappedEnumMember(EnumMember):
|
||
|
"""
|
||
|
Used to define a member whose value maps to an XML attribute value.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, name, value, xml_value, docstring):
|
||
|
super(XmlMappedEnumMember, self).__init__(name, value, docstring)
|
||
|
self._xml_value = xml_value
|
||
|
|
||
|
def add_to_enum(self, clsdict):
|
||
|
"""
|
||
|
Compile XML mappings in addition to base add behavior.
|
||
|
"""
|
||
|
super(XmlMappedEnumMember, self).add_to_enum(clsdict)
|
||
|
self.register_xml_mapping(clsdict)
|
||
|
|
||
|
def register_xml_mapping(self, clsdict):
|
||
|
"""
|
||
|
Add XML mappings to the enumeration class state for this member.
|
||
|
"""
|
||
|
member_to_xml = self._get_or_add_member_to_xml(clsdict)
|
||
|
member_to_xml[self.value] = self.xml_value
|
||
|
xml_to_member = self._get_or_add_xml_to_member(clsdict)
|
||
|
xml_to_member[self.xml_value] = self.value
|
||
|
|
||
|
@property
|
||
|
def xml_value(self):
|
||
|
"""
|
||
|
The XML attribute value that corresponds to this enumeration value
|
||
|
"""
|
||
|
return self._xml_value
|
||
|
|
||
|
@staticmethod
|
||
|
def _get_or_add_member_to_xml(clsdict):
|
||
|
"""
|
||
|
Add the enum -> xml value mapping to the enumeration class state
|
||
|
"""
|
||
|
if "_member_to_xml" not in clsdict:
|
||
|
clsdict["_member_to_xml"] = dict()
|
||
|
return clsdict["_member_to_xml"]
|
||
|
|
||
|
@staticmethod
|
||
|
def _get_or_add_xml_to_member(clsdict):
|
||
|
"""
|
||
|
Add the xml -> enum value mapping to the enumeration class state
|
||
|
"""
|
||
|
if "_xml_to_member" not in clsdict:
|
||
|
clsdict["_xml_to_member"] = dict()
|
||
|
return clsdict["_xml_to_member"]
|