-
-
Save rmorshea/acd061cfedb701469989154675aeb73e to your computer and use it in GitHub Desktop.
auto docs for traitlets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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