Skip to content

Commit

Permalink
Merge pull request #62 from zopefoundation/issue60
Browse files Browse the repository at this point in the history
Add more information to fields in getDoc().
  • Loading branch information
jamadden committed Sep 7, 2018
2 parents 0a719f2 + 295e128 commit 601dd92
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 1 deletion.
10 changes: 10 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@
<https://github.com/zopefoundation/zope.schema/issues/57>`_.


- Make ``Field.getDoc()`` return more information about the properties
of the field, such as its required and readonly status. Subclasses
can add more information using the new method
``Field.getExtraDocLines()``. This is used to generate Sphinx
documentation when using `repoze.sphinx.autointerface
<https://pypi.org/project/repoze.sphinx.autointerface/>`_. See
`issue 60
<https://github.com/zopefoundation/zope.schema/issues/60>`_.


4.5.0 (2017-07-10)
==================

Expand Down
9 changes: 8 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Interfaces
.. autointerface:: zope.schema.interfaces.IInterfaceField
.. autointerface:: zope.schema.interfaces.IBool
.. autointerface:: zope.schema.interfaces.IObject
.. autointerface:: zope.schema.interfaces.IDict


Strings
-------
Expand Down Expand Up @@ -76,6 +76,13 @@ Collections
.. autointerface:: zope.schema.interfaces.ISet
.. autointerface:: zope.schema.interfaces.IFrozenSet

Mappings
~~~~~~~~
.. autointerface:: zope.schema.interfaces.IMapping
.. autointerface:: zope.schema.interfaces.IMutableMapping
.. autointerface:: zope.schema.interfaces.IDict


Events
------

Expand Down
113 changes: 113 additions & 0 deletions src/zope/schema/_bootstrapfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import decimal
import fractions
import numbers
import sys
import threading
from math import isinf

Expand All @@ -26,6 +27,7 @@
from zope.interface import Interface
from zope.interface import providedBy
from zope.interface import implementer
from zope.interface.interface import InterfaceClass
from zope.interface.interfaces import IInterface
from zope.interface.interfaces import IMethod

Expand Down Expand Up @@ -118,6 +120,62 @@ def getFields(schema):
fields[name] = attr
return fields

class _DocStringHelpers(object):
# Namespace object to hold methods related to ReST formatting
# docstrings

@staticmethod
def docstring_to_lines(docstring):
# Similar to what sphinx.utils.docstrings.prepare_docstring
# does. Strip leading equal whitespace, accounting for an initial line
# that might not have any. Return a list of lines, with a trailing
# blank line.
lines = docstring.expandtabs().splitlines()
# Find minimum indentation of any non-blank lines after ignored lines.

margin = sys.maxsize
for line in lines[1:]:
content = len(line.lstrip())
if content:
indent = len(line) - content
margin = min(margin, indent)
# Remove indentation from first ignored lines.
if len(lines) >= 1:
lines[0] = lines[0].lstrip()

if margin < sys.maxsize:
for i in range(1, len(lines)):
lines[i] = lines[i][margin:]
# Remove any leading blank lines.
while lines and not lines[0]:
lines.pop(0)
#
lines.append('')
return lines

@staticmethod
def make_class_directive(kind):
mod = kind.__module__
if kind.__module__ in ('__builtin__', 'builtins'):
mod = ''
if mod in ('zope.schema._bootstrapfields', 'zope.schema._field'):
mod = 'zope.schema'
mod += '.' if mod else ''
return ':class:`%s%s`' % (mod, kind.__name__)

@classmethod
def make_field(cls, name, value):
return ":%s: %s" % (name, value)

@classmethod
def make_class_field(cls, name, kind):
if isinstance(kind, (type, InterfaceClass)):
return cls.make_field(name, cls.make_class_directive(kind))
if isinstance(kind, tuple):
return cls.make_field(
name,
', '.join([cls.make_class_directive(t) for t in kind]))


class Field(Attribute):

Expand Down Expand Up @@ -182,6 +240,9 @@ def __init__(self, title=u'', description=u'', __name__='',
__doc__ = ''
if title:
if description:
# Fix leading whitespace that occurs when using multi-line
# strings.
description = '\n'.join(_DocStringHelpers.docstring_to_lines(description)[:-1])
__doc__ = "%s\n\n%s" % (title, description)
else:
__doc__ = title
Expand Down Expand Up @@ -286,6 +347,53 @@ def set(self, object, value):
object.__class__.__name__))
setattr(object, self.__name__, value)

def getExtraDocLines(self):
"""
Return a list of ReST formatted lines that will be added
to the docstring returned by :meth:`getDoc`.
By default, this will include information about the various
properties of this object, such as required and readonly status,
required type, and so on.
This implementation uses a field list for this.
Subclasses may override or extend.
.. versionadded:: 4.6.0
"""

lines = []
lines.append(_DocStringHelpers.make_class_field('Implementation', type(self)))
lines.append(_DocStringHelpers.make_field("Read Only", self.readonly))
lines.append(_DocStringHelpers.make_field("Required", self.required))
if self.defaultFactory:
lines.append(_DocStringHelpers.make_field("Default Factory", repr(self.defaultFactory)))
else:
lines.append(_DocStringHelpers.make_field("Default Value", repr(self.default)))

if self._type:
lines.append(_DocStringHelpers.make_class_field("Allowed Type", self._type))

# key_type and value_type are commonly used, but don't
# have a common superclass to add them, so we do it here.
# Using a rubric produces decent formatting
for name, rubric in (('key_type', 'Key Type'),
('value_type', 'Value Type')):
field = getattr(self, name, None)
if hasattr(field, 'getDoc'):
lines.append(".. rubric:: " + rubric)
lines.append(field.getDoc())

return lines

def getDoc(self):
doc = super(Field, self).getDoc()
lines = _DocStringHelpers.docstring_to_lines(doc)
lines += self.getExtraDocLines()
lines.append('')

return '\n'.join(lines)

class Container(Field):

Expand Down Expand Up @@ -806,6 +914,11 @@ def __init__(self, schema=_NotGiven, **kw):
self.validate_invariants = kw.pop('validate_invariants', True)
super(Object, self).__init__(**kw)

def getExtraDocLines(self):
lines = super(Object, self).getExtraDocLines()
lines.append(_DocStringHelpers.make_class_field("Must Provide", self.schema))
return lines

def _validate(self, value):
super(Object, self)._validate(value)

Expand Down
63 changes: 63 additions & 0 deletions src/zope/schema/tests/test__bootstrapfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,64 @@ def _getTargetInterface(self):
from zope.schema.interfaces import IField
return IField

def test_getDoc(self):
import textwrap
field = self._makeOne(readonly=True, required=False)
doc = field.getDoc()
self.assertIn(':Read Only: True', doc)
self.assertIn(':Required: False', doc)
self.assertIn(":Default Value:", doc)
self.assertNotIn(':Default Factory:', doc)

field._type = str
doc = field.getDoc()
self.assertIn(':Allowed Type: :class:`str`', doc)
self.assertNotIn(':Default Factory:', doc)

field.defaultFactory = 'default'
doc = field.getDoc()
self.assertNotIn(":Default Value:", doc)
self.assertIn(':Default Factory:', doc)

field._type = (str, object)
doc = field.getDoc()
self.assertIn(':Allowed Type: :class:`str`, :class:`object`', doc)
self.assertNotIn('..rubric', doc)

# value_type and key_type are automatically picked up
field.value_type = self._makeOne()
field.key_type = self._makeOne()
doc = field .getDoc()
self.assertIn('.. rubric:: Key Type', doc)
self.assertIn('.. rubric:: Value Type', doc)

field = self._makeOne(title=u'A title', description=u"""Multiline description.
Some lines have leading whitespace.
It gets stripped.
""")

doc = field.getDoc()
self.assertEqual(
field.getDoc(),
textwrap.dedent("""\
A title
Multiline description.
Some lines have leading whitespace.
It gets stripped.
:Implementation: :class:`zope.schema.Field`
:Read Only: False
:Required: True
:Default Value: None
""")
)


def test_ctor_defaults(self):

field = self._makeOne()
Expand Down Expand Up @@ -1480,6 +1538,11 @@ class Favorites(object):
self.assertEqual(bad_choices, e.value)
self.assertEqual(['choices'], list(e.schema_errors))

def test_getDoc(self):
field = self._makeOne()
doc = field.getDoc()
self.assertIn(":Must Provide: :class:", doc)


class DummyInst(object):
missing_value = object()
Expand Down

0 comments on commit 601dd92

Please sign in to comment.