# 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"]