Skip to content

Instantly share code, notes, and snippets.

@rmorshea
Last active July 18, 2016 23:18
Show Gist options
  • Save rmorshea/acd061cfedb701469989154675aeb73e to your computer and use it in GitHub Desktop.
Save rmorshea/acd061cfedb701469989154675aeb73e to your computer and use it in GitHub Desktop.
auto docs for traitlets
import re
import sys
import inspect
import types
from traitlets import *
from six import with_metaclass
import string
_vformat = string.Formatter().vformat
def format_map(string, *args, **kwargs):
return _vformat(string, args, kwargs)
# - - - - - - - - - - - - - -
# Document Section Descriptor
# - - - - - - - - - - - - - -
class DocumentSection(object):
source = None
section = None
_renderer = None
_formater = None
# default to simple string coesion
def __init__(self, source):
if isinstance(source, types.FunctionType):
self._renderer = source
else:
self.source = source
def __call__(self, func):
self._renderer = func
return self
def __get__(self, inst, cls=None):
if inst is None:
return self
elif self.section is None:
raise RuntimeError("Improper metaclass setup for %s descriptor on type object %s" %
(self.__class__.__name__, inst.__class__.__name__))
elif self.section in inst._content:
return inst._content[self.section]
elif self.source is None:
value = self._renderer(inst, inst.source)
else:
value = getattr(inst.source, self.source, None)
if self._renderer is not None:
value = self._renderer(inst, value)
inst._content[self.section] = value
return value
def format(self, func):
self._formater = func
return self
def formater(self, inst):
if self._formater is None:
return str(getattr(inst, self.section))
else:
return self._formater(inst, getattr(inst, self.section))
def metasetup(self, cls, name):
self.this_cls = cls
self.section = name
@staticmethod
def new(getter, parser):
return DocumentSection(getter).parse(parser)
def docsec(section):
"""Simple decorator returning a DocumentSection with a parser"""
return DocumentSection(section)
# - - - - - - - - - - - - - -
# DocLog Object and Metaclass
# - - - - - - - - - - - - - -
class MetaDocLog(type):
def __init__(cls, name, bases, classdict):
"""Simple metaclass for assigning section names"""
if cls._from_class and cls._from_insts:
# simple sanity check - conflict causes confusing behavior
raise RuntimeError("Cannot log only from instance sources, but also"
" from classes ('_from_insts' and '_from_class' are both True)")
for k, v in getmembers(cls):
if isinstance(v, DocumentSection):
v.metasetup(cls, k)
class DocLog(with_metaclass(MetaDocLog, object)):
logs_type = None
template = None
_from_class = False
_from_insts = False
_content = None
_indent = 4
def __init__(self, source):
if inspect.isclass(source):
if self._from_insts:
raise TypeError("Expected a%s%s instance, not %r" % 'n' if
self.logs_type is None else ' ', '' if self.logs_type
is None else self.logs_type.__name__, source)
cls = source
else:
cls = source.__class__
if self.logs_type is not None and not issubclass(cls, self.logs_type):
raise TypeError("A '%s' makes autodocs for '%s', not %r"
% (self.__class__.__name__, self.logs_type.__name__, source))
self.source = cls if self._from_class else source
self._content = {}
def __iter__(self):
return iter(self.content)
def __len__(self):
return len(self.content)
def __repr__(self):
return self.__class__.__name__ + '(' + repr(self.content) + ')'
@classmethod
def sections(cls):
return dict([member for member in getmembers(cls) if
isinstance(member[1], DocumentSection)])
@property
def content(self):
sections = self.sections()
if len(self._content) != len(sections):
c = {}
for name in sections:
c[name] = getattr(self, name)
else:
c = self._content.copy()
return c
def reset(self, *sections):
"""Force all, or the given document sections, to be recomposed"""
if self.template is None:
raise ValueError("Type object '%s' has no defined"
" template" % self.__class__.__name__)
if len(sections):
for s in secitons:
del self._content[s]
else:
self._content = {}
def formated_content(self, name, tabs=0):
return self.sections()[name].formater(self, tabs)
def document(self):
"""Return a docstring generated by rendering the template"""
if self.template is None:
raise ValueError("No document template for %s" % self.__class__.__name__)
document = self.template[:]
sections = self.sections()
newlines = [m.start() for m in re.finditer(r'\n', self.template)]
for match in re.findall(r'({[^{}]*?})', self.template):
spaces, tabs = 0, 0
last_nl = self._last_newline(match)
for char in self.template[last_nl+1:]:
if char == ' ':
spaces += 1
elif char == '\t':
tabs += 1
else:
break
# final tab count with spaces
tabs += spaces/self._indent
if match[1:-1] in sections:
# if section name only, call section formater
value = sections[match[1:-1]].formater(self)
if not isinstance(value, six.types.StringTypes):
raise TypeError("Document section formaters "
"must return strings, not %r" % value)
value.replace('\n', '\n' + '\t'*tabs)
document = document.replace(match, value)
else:
# else perform eval on inner with
# raw content as globals (same as
# str.format with added features)
try:
value = eval(match[1:-1], self.content)
except:
pass
else:
document = document.replace(match, value)
return document
def _last_newline(self, substring):
"""Find the template's last newline character prior to the substring"""
last_newline = 0
substring_index = self.template.index(substring)
for i in (m.start() for m in re.finditer(r'\n', self.template)):
if substring_index < i:
break
else:
last_newline = i
else:
last_newline = len(self.template) - self.template[::-1].index('\n')
return last_newline
class FormatRepr(object):
def __init__(self, raw, doc=None):
if doc is None:
doc = str(raw)
self._raw = raw
self._doc = doc
def __getitem__(self, key):
return self._raw[key]
def __getattr__(self, key):
return getattr(self._raw, key)
def __repr__(self):
return self._doc
class FormatDict(dict):
def __missing__(self, key):
return '{%s}' % key
class DocLogMap(object):
def __init__(self, doclog_types):
self._d = {}
for dlt in doclog_types:
if not inspect.isclass(dlt) and not issubclass(dlt, DocLog):
raise TypeError("Expected subclasses of DocLog, not %r" % dlt)
if 'logs_type' not in dlt.__dict__:
raise TypeError("The 'logs_type' attribute is not "
"explicitely defined for '%s'" % dlt.__name__)
self._d[self._to_name(dlt.logs_type)] = dlt
def __iter__(self):
return iter(self._d)
def __call__(self, obj):
"""A factory returning a correctly mapped DocLog instance"""
return self[obj](obj)
def __getitem__(self, obj):
for c in self._to_class(obj).mro():
dlt = self._get(c)
if dlt is not None:
return dlt
else:
raise KeyError(repr(cls))
def get(self, obj, default=None):
for c in self._to_class(obj).mro():
dlt = self._get(c)
if dlt is not None:
return dlt
else:
return default
def _get(self, cls):
return self._d.get(self._to_name(cls))
@staticmethod
def _to_class(obj):
if inspect.isclass(obj):
return obj
else:
return obj.__class__
@staticmethod
def _to_name(key):
if inspect.isclass(key):
name = key.__module__ + '.' + key.__name__
else:
TypeError("Expected a class")
return name
def __repr__(self):
return repr(self._d)
# - - - - - - - - - - - - -
# TraitType DocLog Objects
# - - - - - - - - - - - - -
class TraitDocLog(DocLog):
_from_insts = True
logs_type = TraitType
indent = 4
template = (
"""{name}: {info}
:help: {help}
- trait metadata
{metadata}"""
)
name = DocumentSection('name')
read_only = DocumentSection('read_only')
allow_none = DocumentSection('allow_none')
@docsec('info')
def info(self, info_generator):
return info_generator()
@docsec
def help(self, trait):
return self.metadata.pop('help', None)
@docsec('default_value')
def default_value(self, value):
if 'default_value' in self.metadata:
# pop from given metadata if present
return self.metadata.pop('default_value')
else:
return value
@docsec('metadata')
def metadata(self, metadata):
return metadata.copy()
@metadata.format
def metadata(self, value):
final = ''
for k, v in self.metadata.items():
final += '* %s : %r' % (k, v)
return final
class ClassBasedTraitDocLog(TraitDocLog):
logs_type = ClassBasedTraitType
klass = DocumentSection('klass')
class TypeTraitDocLog(ClassBasedTraitDocLog):
template = (
"""{name}: {info}
:help: {help}
subclass of: {klass}
- trait metadata
{metadata}"""
)
class InstanceTraitDocLog(ClassBasedTraitType):
template = (
"""{name}: {info}
:help: {help}
instance of: {klass}
- trait metadata
{metadata}"""
)
class ThisTraitDocLog(ClassBasedTraitType)
class UnionTraitDocLog(TraitDocLog):
logs_type = Union
trait_types = DocumentSection('trait_types')
# not stored in trait_doc_log_map
class NumberTraitDocLog(TraitDocLog):
minimum = DocumentSection('min')
maximum = DocumentSection('max')
class IntTraitDocLog(NumberTraitDocLog):
logs_type = Int
class FloatTraitDocLog(NumberTraitDocLog):
logs_type = Float
class EnumTraitDocLog(TraitDocLog):
logs_type = Enum
values = DocumentSection('values')
class ContainerTraitDocLog(ClassBasedTraitDocLog):
logs_type = Container
trait = DocumentSection('_trait')
class ListTraitDocLog(ContainerTraitDocLog):
minimum_length = DocumentSection('_minlen')
maximum_length = DocumentSection('_maxlen')
class TupleTraitDocLog(ContainerTraitDocLog):
trait = None
traits = DocumentSection('_traits')
class DictTraitDocLog(Instance):
trait = DocumentSection('_trait')
class UseEnumTraitDoclog(TraitDocLog):
enum_class = DocumentSection('enum_class')
name_prefix = DocumentSection('name_prefix')
# - - - - - - - - - - - -
# HasTraits DocLog Object
# - - - - - - - - - - - -
trait_doclog_mapping = DocLogMap([v for v in globals().values()
if inspect.isclass(v) and issubclass(v, TraitDocLog)
and 'logs_type' in v.__dict__])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment