Skip to content

Commit

Permalink
More work on intro narrative docs. Use Manuel for testing them since …
Browse files Browse the repository at this point in the history
…they will have code snippets.
  • Loading branch information
jamadden committed Jul 23, 2018
1 parent 2215d1b commit d1e27cb
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 11 deletions.
108 changes: 100 additions & 8 deletions docs/basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@
Basic Usage
=============

This document provides an overview of ``nti.externalization`` and
shows some simple examples of its usage.

Reading through the :doc:`glossary` before beginning is highly
recommended.

.. sidebar:: History

This package was originally developed for use in a `Pyramid
Expand All @@ -27,9 +21,15 @@ recommended.
(specifically :mod:`plist <plistlib>` for iOS, which didn't support
`None`, and JSON for browsers.)

This document provides an overview of ``nti.externalization`` and
shows some simple examples of its usage.

Reading through the :doc:`glossary` before beginning is highly
recommended.


Motivation
==========
Motivation and Use-cases
========================

This package provides a supporting framework for transforming to and from
Python objects and an arbitrary binary or textual representation. That
Expand All @@ -42,3 +42,95 @@ that representation include:
objects;
- Using as a human-readable configuration format (with the proper
choice of representation)

We expect that there will be lots of such objects in an application,
and we want to make it as easy as possible to communicate them.
Ideally, we want to be able to completely automate that, handing the
entire task off to this package.

It is also important that when we read external input, we validate
that it meets any internal constraints before further processing. For
example, numbers should be within their allowed range, or references
to other objects should actually result in an object of the expected
type.

Finally, we don't want to have to intermingle any code for reading and
writing objects with our actual business (application) logic. The two
concerns should be kept as separated as possible from our model
objects. Ideally, we should be able to use third-party objects that we
have no control over seamlessly in external and internal data.

Getting Started
===============

In its simplest form, there are two functions you'll use to
externalize and internalize objects::

>>> from nti.externalization import to_external_object
>>> from nti.externalization import update_from_external_object

We can define an object that we want to externalize:

.. code-block:: python
class InternalObject(object):
def __init__(self, id=''):
self._field1 = 'a'
self._field2 = 42
self._id = id
def toExternalObject(self, request=None, **kwargs):
return {'A Letter': self._field1, 'The Number': self._field2}
def __repr__(self):
return '<%s %r letter=%r number=%d>' % (
self.__class__.__name__, self._id, self._field1, self._field2)
.. caution::

The signature for ``toExternalObject`` is poorly defined right now.
The suitable keyword arguments should be enumerated and documented,
but they are not. See https://github.com/NextThought/nti.externalization/issues/54

And we can externalize it with
`~nti.externalization.to_external_object` (we sort it here to be sure
we get consistent output):

>>> sorted(to_external_object(InternalObject()).items())
[('A Letter', 'a'), ('The Number', 42)]

If we want to update it, we need to write the corresponding method:

.. code-block:: python
class UpdateInternalObject(InternalObject):
def updateFromExternalObject(self, external_object, context=None):
self._field1 = external_object['A Letter']
self._field2 = external_object['The Number']
Updating it uses `~nti.externalization.update_from_external_object`:

>>> internal = UpdateInternalObject('internal')
>>> internal
<UpdateInternalObject 'internal' letter='a' number=42>
>>> update_from_external_object(internal, {'A Letter': 'b', 'The Number': 3})
<UpdateInternalObject 'internal' letter='b' number=3>


That's Not Good Enough
----------------------

Notice that we had to define procedurally the input and output steps
in our classes. For some (small) applications, that may be good
enough, but it doesn't come anywhere close to meeting our motivations:

1. By mingling the externalization code into our business objects, it
makes them larger and muddies their true purpose.
2. There's nothing doing any validation. Any such checking is left up
to the object itself.
3. It's manual code to write and test for each of the many objects we
can communicate. There's nothing automatic about it.

Let's see how this package helps us address each of those concerns in turn.
7 changes: 4 additions & 3 deletions docs/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@

internal object

A Python object in the application domain. This may be a
complex object consisting of multiple nested objects. It may
use inheritance. It will implement one or more :term:`schema`.
A Python object in the application domain (sometimes known as a
"model" object). This may be a complex object consisting of
multiple nested objects. It may use inheritance. It will
implement one or more :term:`schema`.

external object

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'fudge',
'nti.testing',
'zope.testrunner',
'manuel',
]


Expand Down
53 changes: 53 additions & 0 deletions src/nti/externalization/tests/test_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""
Tests for the sphinx documentation using `Manuel
<https://pythonhosted.org/manuel/>`_.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function


import doctest
import os.path
import unittest

from zope.testing import renormalizing

import manuel.codeblock
import manuel.doctest
import manuel.testing


def test_suite():
here = os.path.dirname(__file__)
while not os.path.exists(os.path.join(here, 'setup.py')):
here = os.path.join(here, '..')

here = os.path.abspath(here)
docs = os.path.join(here, 'docs')

files_to_test = (
'basics.rst',
)

m = manuel.doctest.Manuel(optionflags=(
doctest.NORMALIZE_WHITESPACE
| doctest.ELLIPSIS
| doctest.IGNORE_EXCEPTION_DETAIL
| renormalizing.IGNORE_EXCEPTION_MODULE_IN_PYTHON2
))
m += manuel.codeblock.Manuel()

suite = unittest.TestSuite()
suite.addTest(
manuel.testing.TestSuite(
m,
*[os.path.join(docs, f) for f in files_to_test]
)
)

return suite

if __name__ == '__main__':
unittest.main()

0 comments on commit d1e27cb

Please sign in to comment.