From 668c8d38b9149a789e62a6ef6c46482a0c542942 Mon Sep 17 00:00:00 2001 From: Grzegorz Bokota Date: Fri, 1 Apr 2022 01:35:39 +0200 Subject: [PATCH] add basic documentation --- .readthedocs.yml | 25 +++++++++++++++ README.rst | 24 ++++++++++++++ docs/Makefile | 20 ++++++++++++ docs/conf.py | 66 ++++++++++++++++++++++++++++++++++++++ docs/index.rst | 26 +++++++++++++++ docs/make.bat | 35 ++++++++++++++++++++ docs/requirements.txt | 5 +++ src/nme/__init__.py | 27 +++++++++++++++- src/nme/_class_register.py | 4 +-- src/nme/_json_hooks.py | 57 +++++++++++++++++++++++++++----- tox.ini | 1 + 11 files changed, 279 insertions(+), 11 deletions(-) create mode 100644 .readthedocs.yml create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..0946788 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,25 @@ +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Build documentation with MkDocs +#mkdocs: +# configuration: mkdocs.yml + +# Optionally build your docs in additional formats such as PDF and ePub +formats: all + +# Optionally set the version of Python and requirements required to build your docs +python: + version: 3.8 + install: + - requirements: docs/requirements.txt + - method: pip + path: .[cbor] diff --git a/README.rst b/README.rst index bd1fe7f..84d6131 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,10 @@ Napari Migration Engine :target: https://github.com/psf/black :alt: Code Style +.. image:: https://readthedocs.org/projects/nme/badge/?version=latest + :target: https://nme.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + This is support package for simplify data serialization and persistance data between sessions and versions. @@ -46,3 +50,23 @@ If You only need to serialize data, then you could use only JSON hooks data2 = json.load(f_p, object_hook=nme_object_hook) assert data == data2 + + +Additional functions +#################### + +* ``rename_key(from_key: str, to_key: str, optional=False) -> Callable[[Dict], Dict]`` - helper + function for rename field migrations. + +* ``update_argument(argument_name:str)(func: Callable) -> Callable`` - decorator to keep backward + compatibility by converting ``dict`` argument to some class base on function type annotation + + +Additional notes +################ + +This package is extracted from `PartSeg`_ +project for simplify reuse it in another projects. + + +.. _PartSeg: https://github.com/4DNucleome/PartSeg diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..cd5f7e8 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,66 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +from nme.version import version + +# -- Project information ----------------------------------------------------- + +project = "nme" +copyright = "2022, Grzegorz Bokota" +author = "Grzegorz Bokota" + +# The full version, including alpha/beta/rc tags +release = version + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx_autodoc_typehints", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "napari" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "numpy": ("https://docs.scipy.org/doc/numpy/", None), + "packaging": ("https://packaging.pypa.io/en/latest/", None), +} diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..fa72e0c --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,26 @@ +.. nme documentation master file, created by + sphinx-quickstart on Fri Apr 1 00:32:04 2022. + You can adapt this file completely to your liking, but it should at least + contain the root ``toctree`` directive. + +Welcome to nme's documentation! +=============================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +.. automodule:: nme + :members: + :show-inheritance: + + .. autodata:: REGISTER + .. autodata:: MigrationInfo + .. autofunction:: nme_object_encoder + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..0f740f3 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,5 @@ +sphinx-autodoc-typehints==1.17.0 +sphinx!=3.0.0, !=3.5.0 +napari-sphinx-theme +numpy +pydantic diff --git a/src/nme/__init__.py b/src/nme/__init__.py index ff73282..1b1a3c4 100644 --- a/src/nme/__init__.py +++ b/src/nme/__init__.py @@ -1,8 +1,19 @@ -from ._class_register import REGISTER, register_class, rename_key, update_argument +from ._class_register import REGISTER, MigrationInfo, MigrationRegistration, register_class, rename_key, update_argument from ._json_hooks import NMEEncoder, nme_object_encoder, nme_object_hook def nme_cbor_encoder(encoder, value): + """ + Cbor encoder hook. Use :py:func:`nme_object_encoder` to encode objects. + + :param encoder: cbor2.Encoder + :param value: object to be encoded + + Examples:: + + with open(path_to_file, "wb") as f_p: + cbor2.dump(data, f_p, default=nme_cbor_encoder) + """ res = nme_object_encoder(value) if res is None: return encoder.encode(value) @@ -10,6 +21,18 @@ def nme_cbor_encoder(encoder, value): def nme_cbor_decoder(decoder, value): + """ + Cbor decoder hook. Use :py:func:`nme_object_hook` to decode objects. + + :param decoder: cbor2.Decoder + :param value: object to be decoded + + Examples:: + + with open(path_to_file, "rb") as f_p: + data = cbor2.load(f_p, object_hook=nme_cbor_decoder) + + """ return nme_object_hook(value) @@ -17,6 +40,8 @@ def nme_cbor_decoder(decoder, value): "register_class", "nme_object_hook", "rename_key", + "MigrationInfo", + "MigrationRegistration", "NMEEncoder", "REGISTER", "update_argument", diff --git a/src/nme/_class_register.py b/src/nme/_class_register.py index 2891326..e386da7 100644 --- a/src/nme/_class_register.py +++ b/src/nme/_class_register.py @@ -191,10 +191,10 @@ def _register_missed(self, class_str): self.register(class_) -# THe global instance of register is use because registration is performed on import time. +# The global instance of register is use because registration is performed on import time. # There should no information storage for objects. REGISTER = MigrationRegistration() -"""Default register to storage class information""" +"""Default register to storage class information. Instance of :py:class:`MigrationRegistration`""" def rename_key(from_key: str, to_key: str, optional=False) -> MigrationCallable: diff --git a/src/nme/_json_hooks.py b/src/nme/_json_hooks.py index 78e222d..1fda277 100644 --- a/src/nme/_json_hooks.py +++ b/src/nme/_json_hooks.py @@ -1,10 +1,9 @@ import dataclasses import enum import json +import typing from pathlib import Path -import numpy as np - from ._class_register import REGISTER, class_to_str try: @@ -16,14 +15,20 @@ class BaseModel: # type: ignore try: - from numpy import ndarray + from numpy import floating, integer, ndarray except ImportError: # pragma: no cover # allow to use in environment without numpy. class ndarray: # type: ignore pass + class integer: # type: ignore + pass + + class floating: # type: ignore + pass -def add_class_info(obj: type, dkt: dict) -> dict: + +def add_class_info(obj: typing.Any, dkt: dict) -> dict: dkt["__class__"] = class_to_str(obj.__class__) dkt["__class_version_dkt__"] = { class_to_str(sup_obj): str(REGISTER.get_version(sup_obj)) @@ -42,7 +47,26 @@ def add_class_info(obj: type, dkt: dict) -> dict: return dkt -def nme_object_encoder(obj): +def nme_object_encoder(obj: typing.Any): + """ + Function changing supported types to basic python types supported by most + serializers and which could be restored by :py:func:`nme_object_hook` function. + + Supported types are: + + * :py:class:`enum.Enum` + * :py:func:`dataclasses.dataclass` + * :py:class:`numpy.ndarray` + * :py:class:`pydantic.BaseModel` + * :py:class:`numpy.integer` (change to pure int) + * :py:class:`numpy.floating` (change to pure float) + * :py:class:`pathlib.Path` (Serialized to string) + * Any class with an ``as_dict`` method. This method should return a dictionary of valid constructor arguments. + + :param obj: object to be encoded. + :return: encoded object for supported types. Otherwise ``None``. + + """ if isinstance(obj, enum.Enum): dkt = {"value": obj.value} return add_class_info(obj, dkt) @@ -65,9 +89,9 @@ def nme_object_encoder(obj): dkt = obj.as_dict() return add_class_info(obj, dkt) - if isinstance(obj, np.integer): + if isinstance(obj, integer): return int(obj) - if isinstance(obj, np.floating): + if isinstance(obj, floating): return float(obj) if isinstance(obj, Path): return str(obj) @@ -75,14 +99,31 @@ def nme_object_encoder(obj): class NMEEncoder(json.JSONEncoder): + """ + JSONEncoder subclass for serializing Python objects into JSON. + For list of supported types check :py:func:`nme_object_encoder` function. + """ + def default(self, o): + """ + Implementation that calls :py:func:`nme_object_encoder` function. + """ val = nme_object_encoder(o) if val is None: return super().default(o) return val -def nme_object_hook(dkt: dict): +def nme_object_hook(dkt: dict) -> typing.Any: + """ + Function restoring supported types from :py:func:`nme_object_encoder` function output. + + If ``dkt`` does not contain ``__class__`` key, it is returned as is. + + If the restoting object fails then function return dict with ``"__error__"`` key. + + :param dkt: dictionary with data to restore. + """ if "__error__" in dkt: dkt.pop("__error__") # different environments without same plugins installed if "__class__" in dkt: diff --git a/tox.ini b/tox.ini index 1caa7d9..5abe9ab 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ python = [testenv] extras = test + cbor commands = python -m pytest src/tests