From 4a0ca5e3909a5384a91d44bdce3a40acac732f21 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Fri, 1 Oct 2021 08:35:10 -0400 Subject: [PATCH] Documentation updates for typing. The changes to `api.rst` are particularly important since that is where I describe the externally available type annotations. --- docs/api.rst | 64 +++++++++++++- docs/conf.py | 31 ++++++- docs/history.rst | 4 + sprockets/mixins/mediatype/content.py | 101 +++++++++++----------- sprockets/mixins/mediatype/handlers.py | 28 +++--- sprockets/mixins/mediatype/transcoders.py | 8 +- 6 files changed, 157 insertions(+), 79 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 3374348..b1b0f87 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,7 +1,27 @@ API Documentation ================= + .. currentmodule:: sprockets.mixins.mediatype.content +The easiest way to use this library is to: + +#. call :func:`.install` when you create your application instance and specify a + default content type +#. call :func:`.add_transcoder` to install transcoders for the content types that + you support -- use :func:`.add_binary_content_type` and/or + :func:`.add_text_content_type` if you don't want to define a + :class:`~sprockets.mixins.mediatype.type_info.Transcoder` class. +#. include :class:`.ContentMixin` in your handler's inheritance chain +#. call :meth:`~.ContentMixin.get_request_body` to retrieve a request body + sent in any of the supported content types +#. call :meth:`~.ContentMixin.send_response` to send a response in any of the + supported content types + +The :class:`.ContentMixin` will take care of inspecting the :http:header:`Content-Type` +header and deserialize the request as well as implementing the +:rfc:`proactive content negotiation algorithm <7231#section-3.4.1>` described in +:rfc:`7231` to serialize a response object appropriately. + Content Type Handling --------------------- .. autoclass:: ContentMixin @@ -11,7 +31,7 @@ Content Type Registration ------------------------- .. autofunction:: install -.. autofunction:: get_settings +.. autofunction:: add_transcoder .. autofunction:: set_default_content_type @@ -19,7 +39,7 @@ Content Type Registration .. autofunction:: add_text_content_type -.. autofunction:: add_transcoder +.. autofunction:: get_settings .. autoclass:: ContentSettings :members: @@ -33,3 +53,43 @@ Bundled Transcoders .. autoclass:: MsgPackTranscoder :members: + +.. _type-info: + +Python Type Information +----------------------- +The ``sprockets.mixins.mediatype.type_info`` module contains a number of +convenience type definitions for those you you who take advantage of type +annotations. + +.. currentmodule:: sprockets.mixins.mediatype.type_info + +Interface Types +~~~~~~~~~~~~~~~ + +.. autoclass:: Transcoder + :members: + +.. autodata:: Serializable + +.. autodata:: Deserialized + +Convenience Types +~~~~~~~~~~~~~~~~~ + +.. autodata:: PackBFunction + +.. autodata:: UnpackBFunction + +.. autodata:: DumpSFunction + +.. autodata:: LoadSFunction + +Contract Types +~~~~~~~~~~~~~~ + +.. autoclass:: HasSettings + :members: + +.. autoclass:: DefinesIsoFormat + :members: diff --git a/docs/conf.py b/docs/conf.py index cfd255e..f1a18bb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,10 +3,7 @@ import os import pkg_resources needs_sphinx = '4.0' -extensions = [ - 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', - 'sphinx.ext.extlinks', 'sphinxcontrib.httpdomain' -] +extensions = ['sphinx.ext.viewcode', 'sphinxcontrib.httpdomain'] master_doc = 'index' project = 'sprockets.mixins.mediatype' copyright = '2015-2021, AWeber Communications' @@ -24,14 +21,40 @@ html_sidebars = { '**': ['about.html', 'navigation.html'], } +# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html +extensions.append('sphinx.ext.intersphinx') intersphinx_mapping = { + 'ietfparse': ('https://ietfparse.readthedocs.io/en/latest', None), 'python': ('https://docs.python.org/3', None), 'requests': ('https://requests.readthedocs.org/en/latest/', None), 'sprockets': ('https://sprockets.readthedocs.org/en/latest/', None), 'tornado': ('https://www.tornadoweb.org/en/stable/', None), } +# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html +# We need to define type aliases for both the simple name (e.g., Deserialized) +# and the prefixed name (e.g., type_info.Deserialized) since both forms +# appear in the typing annotations. +extensions.append('sphinx.ext.autodoc') +autodoc_type_aliases = { + alias: f'sprockets.mixins.mediatype.type_info.{alias}' + for alias in { + 'DefinesIsoFormat', 'Deserialized', 'DumpSFunction', 'HasSettings', + 'LoadSFunction', 'MsgPackable', 'PackBFunction', 'Serializable', + 'Transcoder', 'UnpackBFunction' + } +} +autodoc_type_aliases.update({ + f'type_info.{alias}': f'sprockets.mixins.mediatype.type_info.{alias}' + for alias in { + 'DefinesIsoFormat', 'Deserialized', 'DumpSFunction', 'HasSettings', + 'LoadSFunction', 'MsgPackable', 'PackBFunction', 'Serializable', + 'Transcoder', 'UnpackBFunction' + } +}) + # https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html +extensions.append('sphinx.ext.extlinks') extlinks = { 'compare': ('https://github.com/sprockets/sprockets.mixins.mediatype' '/compare/%s', '%s') diff --git a/docs/history.rst b/docs/history.rst index 1f6d6cd..5d0fe79 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,6 +1,10 @@ Version History =============== +:compare:`Next <3.0.4...master>` +-------------------------------- +- Add type annotations (see :ref:`type-info`) + :compare:`3.0.4 <3.0.3...3.0.4>` (2 Nov 2020) --------------------------------------------- - Return a "400 Bad Request" when an invalid Content-Type header is received diff --git a/sprockets/mixins/mediatype/content.py b/sprockets/mixins/mediatype/content.py index 65facf9..16cd08d 100644 --- a/sprockets/mixins/mediatype/content.py +++ b/sprockets/mixins/mediatype/content.py @@ -61,13 +61,9 @@ class ContentSettings: The settings instance contains the list of available content types and handlers associated with them. Each handler implements - a simple interface: - - - ``to_bytes(dict, encoding:str) -> bytes`` - - ``from_bytes(bytes, encoding:str) -> dict`` - - Use the :func:`add_binary_content_type` and :func:`add_text_content_type` - helper functions to modify the settings for the application. + the :class:`~sprockets.mixins.mediatype.type_info.Transcoder` + interface. Use :func:`add_transcoder` to add support for a + specific content type to the application. This class acts as a mapping from content-type string to the appropriate handler instance. Add new content types and find @@ -92,7 +88,7 @@ class ContentSettings: return app Of course, that is quite tedious, so use the :class:`.ContentMixin` - instead. + instead of using the settings directly. """ @@ -178,12 +174,13 @@ def get_settings( """ Retrieve the media type settings for a application. - :param tornado.web.Application application: - :keyword bool force_instance: if :data:`True` then create the + :param application: + :param force_instance: if :data:`True` then create the instance if it does not exist - :return: the content settings instance - :rtype: sprockets.mixins.mediatype.content.ContentSettings + :return: the content settings instance or :data:`None` if + `force_instance` is not :data:`True` and :func:`.install` + has not been called """ try: @@ -200,12 +197,12 @@ def add_binary_content_type(application: type_info.HasSettings, """ Add handler for a binary content type. - :param tornado.web.Application application: the application to modify - :param str content_type: the content type to add + :param application: the application to modify + :param content_type: the content type to add :param pack: function that packs a dictionary to a byte string. - ``pack(dict) -> bytes`` + See :any:`type_info.PackBFunction` :param unpack: function that takes a byte string and returns a - dictionary. ``unpack(bytes) -> dict`` + dictionary. See :any:`type_info.UnpackBFunction` """ add_transcoder(application, @@ -219,13 +216,13 @@ def add_text_content_type(application: type_info.HasSettings, """ Add handler for a text content type. - :param tornado.web.Application application: the application to modify - :param str content_type: the content type to add - :param str default_encoding: encoding to use when one is unspecified + :param application: the application to modify + :param content_type: the content type to add + :param default_encoding: encoding to use when one is unspecified :param dumps: function that dumps a dictionary to a string. - ``dumps(dict, encoding:str) -> str`` + See :any:`type_info.DumpSFunction` :param loads: function that loads a dictionary from a string. - ``loads(str, encoding:str) -> dict`` + See :any:`type_info.LoadSFunction` Note that the ``charset`` parameter is stripped from `content_type` if it is present. @@ -246,32 +243,16 @@ def add_transcoder(application: type_info.HasSettings, """ Register a transcoder for a specific content type. - :param tornado.web.Application application: the application to modify - :param transcoder: object that translates between :class:`bytes` and - :class:`object` instances - :param str content_type: the content type to add. If this is - unspecified or :data:`None`, then the transcoder's ``content_type`` - attribute is used. + :param application: the application to modify + :param transcoder: object that translates between :class:`bytes` + and object instances + :param content_type: the content type to add. If this is + unspecified or :data:`None`, then the transcoder's + ``content_type`` attribute is used. - The `transcoder` instance is required to implement the following - simple protocol: - - .. attribute:: transcoder.content_type - - :class:`str` that identifies the MIME type that the transcoder - implements. - - .. method:: transcoder.to_bytes(inst_data, encoding=None) -> bytes - - :param object inst_data: the object to encode - :param str encoding: character encoding to apply or :data:`None` - :returns: the encoded :class:`bytes` instance - - .. method:: transcoder.from_bytes(data_bytes, encoding=None) -> object - - :param bytes data_bytes: the :class:`bytes` instance to decode - :param str encoding: character encoding to use or :data:`None` - :returns: the decoded :class:`object` instance + The `transcoder` instance is required to implement the + :class:`~sprockets.mixins.mediatype.type_info.Transcoder` + protocol. """ settings = get_settings(application, force_instance=True) @@ -284,9 +265,9 @@ def set_default_content_type(application: type_info.HasSettings, """ Store the default content type for an application. - :param tornado.web.Application application: the application to modify - :param str content_type: the content type to default to - :param str|None encoding: encoding to use when one is unspecified + :param application: the application to modify + :param content_type: the content type to default to + :param encoding: encoding to use when one is unspecified """ settings = get_settings(application, force_instance=True) @@ -322,6 +303,17 @@ class ContentMixin(web.RequestHandler): def get_response_content_type(self) -> typing.Union[str, None]: """Select the content type will be used in the response. + + This method implements proactive content negotiation as + described in :rfc:`7231#section-3.4.1` using the + :http:header:`Accept` request header or the configured + default content type if the header is not present. The + selected response type is cached and returned. It will + be used when :meth:`.send_response` is called. + + Note that this method is called by :meth:`.send_response` + so you will seldom need to call it directly. + """ if self._best_response_match is None: settings = get_settings(self.application, force_instance=True) @@ -346,7 +338,7 @@ class ContentMixin(web.RequestHandler): """ Fetch (and cache) the request body as a dictionary. - :raise web.HTTPError: + :raise tornado.web.HTTPError: - if the content type cannot be matched, then the status code is set to 415 Unsupported Media Type. - if decoding the content body fails, then the status code is @@ -390,10 +382,15 @@ class ContentMixin(web.RequestHandler): """ Serialize and send ``body`` in the response. - :param dict body: the body to serialize - :param bool set_content_type: should the :http:header:`Content-Type` + :param body: the body to serialize + :param set_content_type: should the :http:header:`Content-Type` header be set? Defaults to :data:`True` + The transcoder for the response is selected by calling + :meth:`.get_response_content_type` which chooses an + appropriate transcoder based on the :http:header:`Accept` + header from the request. + """ settings = get_settings(self.application, force_instance=True) # TODO -- account for get_response_type returning None diff --git a/sprockets/mixins/mediatype/handlers.py b/sprockets/mixins/mediatype/handlers.py index addd0ca..2361668 100644 --- a/sprockets/mixins/mediatype/handlers.py +++ b/sprockets/mixins/mediatype/handlers.py @@ -20,7 +20,7 @@ class BinaryContentHandler: """ Pack and unpack binary types. - :param str content_type: registered content type + :param content_type: registered content type :param pack: function that transforms an object instance into :class:`bytes` :param unpack: function that transforms :class:`bytes` @@ -43,8 +43,8 @@ class BinaryContentHandler: """ Transform an object into :class:`bytes`. - :param object inst_data: object to encode - :param str encoding: ignored + :param inst_data: object to encode + :param encoding: ignored :returns: :class:`tuple` of the selected content type and the :class:`bytes` representation of `inst_data` @@ -59,11 +59,8 @@ class BinaryContentHandler: """ Get an object from :class:`bytes` - :param bytes data_bytes: stream of bytes to decode - :param str encoding: ignored - :param dict content_parameters: optional :class:`dict` of - content type parameters from the :mailheader:`Content-Type` - header + :param data_bytes: stream of bytes to decode + :param encoding: ignored :returns: decoded :class:`object` instance """ @@ -74,12 +71,12 @@ class TextContentHandler: """ Transcodes between textual and object representations. - :param str content_type: registered content type + :param content_type: registered content type :param dumps: function that transforms an object instance into a :class:`str` :param loads: function that transforms a :class:`str` into an object instance - :param str default_encoding: encoding to apply when + :param default_encoding: encoding to apply when transcoding from the underlying body :class:`byte` instance @@ -104,8 +101,8 @@ class TextContentHandler: """ Transform an object into :class:`bytes`. - :param object inst_data: object to encode - :param str encoding: character set used to encode the bytes + :param inst_data: object to encode + :param encoding: character set used to encode the bytes returned from the ``dumps`` function. This defaults to :attr:`default_encoding` :returns: :class:`tuple` of the selected content @@ -125,13 +122,10 @@ class TextContentHandler: """ Get an object from :class:`bytes` - :param bytes data: stream of bytes to decode - :param str encoding: character set used to decode the incoming + :param data: stream of bytes to decode + :param encoding: character set used to decode the incoming bytes before calling the ``loads`` function. This defaults to :attr:`default_encoding` - :param dict content_parameters: optional :class:`dict` of - content type parameters from the :mailheader:`Content-Type` - header :returns: decoded :class:`object` instance """ diff --git a/sprockets/mixins/mediatype/transcoders.py b/sprockets/mixins/mediatype/transcoders.py index 844dc84..464a0d9 100644 --- a/sprockets/mixins/mediatype/transcoders.py +++ b/sprockets/mixins/mediatype/transcoders.py @@ -26,10 +26,10 @@ class JSONTranscoder(handlers.TextContentHandler): """ JSON transcoder instance. - :param str content_type: the content type that this encoder instance + :param content_type: the content type that this encoder instance implements. If omitted, ``application/json`` is used. This is passed directly to the ``TextContentHandler`` initializer. - :param str default_encoding: the encoding to use if none is specified. + :param default_encoding: the encoding to use if none is specified. If omitted, this defaults to ``utf-8``. This is passed directly to the ``TextContentHandler`` initializer. @@ -77,7 +77,7 @@ class JSONTranscoder(handlers.TextContentHandler): """ Called to encode unrecognized object. - :param object obj: the object to encode + :param obj: the object to encode :return: the encoded object :raises TypeError: when `obj` cannot be encoded @@ -114,7 +114,7 @@ class MsgPackTranscoder(handlers.BinaryContentHandler): """ Msgpack Transcoder instance. - :param str content_type: the content type that this encoder instance + :param content_type: the content type that this encoder instance implements. If omitted, ``application/msgpack`` is used. This is passed directly to the ``BinaryContentHandler`` initializer.