From d3e4fbb3114e4be767eec30c85e13a73892f92a7 Mon Sep 17 00:00:00 2001 From: James <50501825+Gobot1234@users.noreply.github.com> Date: Sun, 20 Sep 2020 21:00:02 +0100 Subject: [PATCH] Add Documentation (#125) Add sphinx docs with readthedocs integration. Docs can be built locally with `poe docs`. --- CONTRIBUTING.md => .github/CONTRIBUTING.md | 0 .github/workflows/code-quality.yml | 10 +- .readthedocs.yml | 17 ++ README.md | 2 +- docs/api.rst | 31 +++ docs/conf.py | 60 ++++ docs/index.rst | 33 +++ docs/migrating.rst | 157 +++++++++++ docs/quick-start.rst | 192 +++++++++++++ docs/upgrading.md | 16 -- poetry.lock | 308 ++++++++++++++++++--- pyproject.toml | 3 + src/betterproto/__init__.py | 179 ++++++++++-- src/betterproto/casing.py | 63 ++++- 14 files changed, 973 insertions(+), 98 deletions(-) rename CONTRIBUTING.md => .github/CONTRIBUTING.md (100%) create mode 100644 .readthedocs.yml create mode 100644 docs/api.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/migrating.rst create mode 100644 docs/quick-start.rst delete mode 100644 docs/upgrading.md diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 8ab34bd..03221ab 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -9,8 +9,8 @@ on: - '**' jobs: - black: - name: Black + check-formatting: + name: Check code/doc formatting runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -18,3 +18,9 @@ jobs: uses: lgeiger/black-action@master with: args: --check src/ tests/ + + - name: Install rST dependcies + run: python -m pip install doc8 + - name: Lint documentation for errors + run: python -m doc8 docs --max-line-length 88 --ignore-path-errors "docs/migrating.rst;D001" + # it has a table which is longer than 88 characters long diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..7a0b3b7 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,17 @@ +version: 2 +formats: [] + +build: + image: latest + +sphinx: + configuration: docs/conf.py + fail_on_warning: false + +python: + version: 3.7 + install: + - method: pip + path: . + extra_requirements: + - dev \ No newline at end of file diff --git a/README.md b/README.md index 2f3b95c..c70aca7 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ datetime.datetime(2019, 1, 1, 11, 59, 58, 800000, tzinfo=datetime.timezone.utc) ## Development - _Join us on [Slack](https://join.slack.com/t/betterproto/shared_invite/zt-f0n0uolx-iN8gBNrkPxtKHTLpG3o1OQ)!_ -- _See how you can help → [Contributing](CONTRIBUTING.md)_ +- _See how you can help → [Contributing](.github/CONTRIBUTING.md)_ ### Requirements diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..ceae2d2 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,31 @@ +.. currentmodule:: betterproto + +API reference +============= + +The following document outlines betterproto's api. **None** of these classes should be +extended by the user manually. + + +Message +-------- + +.. autoclass:: betterproto.Message + :members: + :special-members: __bytes__ + + +.. autofunction:: betterproto.serialized_on_wire + +.. autofunction:: betterproto.which_one_of + + +Enumerations +------------- + +.. autoclass:: betterproto.Enum() + :members: + + +.. autoclass:: betterproto.Casing() + :members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..564208f --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,60 @@ +# 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 + +# 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 pathlib + +import toml + + +# -- Project information ----------------------------------------------------- + +project = "betterproto" +copyright = "2019 Daniel G. Taylor" +author = "danielgtaylor" +pyproject = toml.load(open(pathlib.Path(__file__).parent.parent / "pyproject.toml")) + + +# The full version, including alpha/beta/rc tags. +release = pyproject["tool"]["poetry"]["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.napoleon", +] + +autodoc_member_order = "bysource" +autodoc_typehints = "none" + +extlinks = { + "issue": ("https://github.com/danielgtaylor/python-betterproto/issues/%s", "GH-"), +} + +# Links used for cross-referencing stuff in other documentation +intersphinx_mapping = { + "py": ("https://docs.python.org/3", None), +} + + +# -- Options for HTML output ------------------------------------------------- + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "friendly" + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. + +html_theme = "sphinx_rtd_theme" diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..dfaa8fc --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,33 @@ +Welcome to betterproto's documentation! +======================================= + +betterproto is a protobuf compiler and interpreter. It improves the experience of using +Protobuf and gRPC in Python, by generating readable, understandable, and idiomatic +Python code, using modern language features. + + +Features: +~~~~~~~~~ + +- Generated messages are both binary & JSON serializable +- Messages use relevant python types, e.g. ``Enum``, ``datetime`` and ``timedelta`` + objects +- ``async``/``await`` support for gRPC Clients +- Generates modern, readable, idiomatic python code + +Contents: +~~~~~~~~~ + +.. toctree:: + :maxdepth: 2 + + quick-start + api + migrating + + +If you still can't find what you're looking for, try in one of the following pages: + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/migrating.rst b/docs/migrating.rst new file mode 100644 index 0000000..0f18eac --- /dev/null +++ b/docs/migrating.rst @@ -0,0 +1,157 @@ +Migrating Guide +=============== + +Google's protocolbuffers +------------------------ + +betterproto has a mostly 1 to 1 drop in replacement for Google's protocolbuffers (after +regenerating your protobufs of course) although there are some minor differences. + +.. note:: + + betterproto implements the same basic methods including: + + - :meth:`betterproto.Message.FromString` + - :meth:`betterproto.Message.SerializeToString` + + for compatibility purposes, however it is important to note that these are + effectively aliases for :meth:`betterproto.Message.parse` and + :meth:`betterproto.Message.__bytes__` respectively. + + +Determining if a message was sent +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes it is useful to be able to determine whether a message has been sent on +the wire. This is how the Google wrapper types work to let you know whether a value is +unset (set as the default/zero value), or set as something else, for example. + +Use ``betterproto.serialized_on_wire(message)`` to determine if it was sent. This is +a little bit different from the official Google generated Python code, and it lives +outside the generated ``Message`` class to prevent name clashes. Note that it only +supports Proto 3 and thus can only be used to check if ``Message`` fields are set. +You cannot check if a scalar was sent on the wire. + +.. code-block:: python + + # Old way (official Google Protobuf package) + >>> mymessage.HasField('myfield') + True + + # New way (this project) + >>> betterproto.serialized_on_wire(mymessage.myfield) + True + + +One-of Support +~~~~~~~~~~~~~~ + +Protobuf supports grouping fields in a oneof clause. Only one of the fields in the group +may be set at a given time. For example, given the proto: + +.. code-block:: proto + + syntax = "proto3"; + + message Test { + oneof foo { + bool on = 1; + int32 count = 2; + string name = 3; + } + } + +You can use ``betterproto.which_one_of(message, group_name)`` to determine which of the +fields was set. It returns a tuple of the field name and value, or a blank string and +``None`` if unset. Again this is a little different than the official Google code +generator: + +.. code-block:: python + + # Old way (official Google protobuf package) + >>> message.WhichOneof("group") + "foo" + + # New way (this project) + >>> betterproto.which_one_of(message, "group") + ("foo", "foo's value") + + +Well-Known Google Types +~~~~~~~~~~~~~~~~~~~~~~~ + +Google provides several well-known message types like a timestamp, duration, and several +wrappers used to provide optional zero value support. Each of these has a special JSON +representation and is handled a little differently from normal messages. The Python +mapping for these is as follows: + ++-------------------------------+-----------------------------------------------+--------------------------+ +| ``Google Message`` | ``Python Type`` | ``Default`` | ++===============================+===============================================+==========================+ +| ``google.protobuf.duration`` | :class:`datetime.timedelta` | ``0`` | ++-------------------------------+-----------------------------------------------+--------------------------+ +| ``google.protobuf.timestamp`` | ``Timezone-aware`` :class:`datetime.datetime` | ``1970-01-01T00:00:00Z`` | ++-------------------------------+-----------------------------------------------+--------------------------+ +| ``google.protobuf.*Value`` | ``Optional[...]``/``None`` | ``None`` | ++-------------------------------+-----------------------------------------------+--------------------------+ +| ``google.protobuf.*`` | ``betterproto.lib.google.protobuf.*`` | ``None`` | ++-------------------------------+-----------------------------------------------+--------------------------+ + + +For the wrapper types, the Python type corresponds to the wrapped type, e.g. +``google.protobuf.BoolValue`` becomes ``Optional[bool]`` while +``google.protobuf.Int32Value`` becomes ``Optional[int]``. All of the optional values +default to None, so don't forget to check for that possible state. + +Given: + +.. code-block:: proto + + syntax = "proto3"; + + import "google/protobuf/duration.proto"; + import "google/protobuf/timestamp.proto"; + import "google/protobuf/wrappers.proto"; + + message Test { + google.protobuf.BoolValue maybe = 1; + google.protobuf.Timestamp ts = 2; + google.protobuf.Duration duration = 3; + } + +You can use it as such: + +.. code-block:: python + + >>> t = Test().from_dict({"maybe": True, "ts": "2019-01-01T12:00:00Z", "duration": "1.200s"}) + >>> t + Test(maybe=True, ts=datetime.datetime(2019, 1, 1, 12, 0, tzinfo=datetime.timezone.utc), duration=datetime.timedelta(seconds=1, microseconds=200000)) + + >>> t.ts - t.duration + datetime.datetime(2019, 1, 1, 11, 59, 58, 800000, tzinfo=datetime.timezone.utc) + + >>> t.ts.isoformat() + '2019-01-01T12:00:00+00:00' + + >>> t.maybe = None + >>> t.to_dict() + {'ts': '2019-01-01T12:00:00Z', 'duration': '1.200s'} + + +[1.2.5] to [2.0.0b1] +-------------------- + +Updated package structures +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Generated code now strictly follows the *package structure* of the ``.proto`` files. +Consequently ``.proto`` files without a package will be combined in a single +``__init__.py`` file. To avoid overwriting existing ``__init__.py`` files, its best +to compile into a dedicated subdirectory. + +Upgrading: + +- Remove your previously compiled ``.py`` files. +- Create a new *empty* directory, e.g. ``generated`` or ``lib/generated/proto`` etc. +- Regenerate your python files into this directory +- Update import statements, e.g. ``import ExampleMessage from generated`` diff --git a/docs/quick-start.rst b/docs/quick-start.rst new file mode 100644 index 0000000..c731ca2 --- /dev/null +++ b/docs/quick-start.rst @@ -0,0 +1,192 @@ +Getting Started +=============== + +Installation +++++++++++++ + +Installation from PyPI is as simple as running: + +.. code-block:: sh + + python3 -m pip install -U betterproto + +If you are using Windows, then the following should be used instead: + +.. code-block:: sh + + py -3 -m pip install -U betterproto + +To include the protoc plugin, install betterproto[compiler] instead of betterproto, +e.g. + +.. code-block:: sh + + python3 -m pip install -U "betterproto[compiler]" + +Compiling proto files ++++++++++++++++++++++ + + +Given you installed the compiler and have a proto file, e.g ``example.proto``: + +.. code-block:: proto + + syntax = "proto3"; + + package hello; + + // Greeting represents a message you can tell a user. + message Greeting { + string message = 1; + } + +To compile the proto you would run the following: + +You can run the following to invoke protoc directly: + +.. code-block:: sh + + mkdir hello + protoc -I . --python_betterproto_out=lib example.proto + +or run the following to invoke protoc via grpcio-tools: + +.. code-block:: sh + + pip install grpcio-tools + python -m grpc_tools.protoc -I . --python_betterproto_out=lib example.proto + + +This will generate ``lib/__init__.py`` which looks like: + +.. code-block:: python + + # Generated by the protocol buffer compiler. DO NOT EDIT! + # sources: example.proto + # plugin: python-betterproto + from dataclasses import dataclass + + import betterproto + + + @dataclass + class Greeting(betterproto.Message): + """Greeting represents a message you can tell a user.""" + + message: str = betterproto.string_field(1) + + +Then to use it: + +.. code-block:: python + + >>> from lib import Greeting + + >>> test = Greeting() + >>> test + Greeting(message='') + + >>> test.message = "Hey!" + >>> test + Greeting(message="Hey!") + + >>> bytes(test) + b'\n\x04Hey!' + >>> Greeting().parse(serialized) + Greeting(message="Hey!") + + +Async gRPC Support +++++++++++++++++++ + +The generated code includes `grpclib `_ based +stub (client) classes for rpc services declared in the input proto files. +It is enabled by default. + + +Given a service definition similar to the one below: + +.. code-block:: proto + + syntax = "proto3"; + + package echo; + + message EchoRequest { + string value = 1; + // Number of extra times to echo + uint32 extra_times = 2; + } + + message EchoResponse { + repeated string values = 1; + } + + message EchoStreamResponse { + string value = 1; + } + + service Echo { + rpc Echo(EchoRequest) returns (EchoResponse); + rpc EchoStream(EchoRequest) returns (stream EchoStreamResponse); + } + +The generated client can be used like so: + +.. code-block:: python + + import asyncio + from grpclib.client import Channel + import echo + + + async def main(): + channel = Channel(host="127.0.0.1", port=50051) + service = echo.EchoStub(channel) + response = await service.echo(value="hello", extra_times=1) + print(response) + + async for response in service.echo_stream(value="hello", extra_times=1): + print(response) + + # don't forget to close the channel when you're done! + channel.close() + + asyncio.run(main()) # python 3.7 only + + # outputs + EchoResponse(values=['hello', 'hello']) + EchoStreamResponse(value='hello') + EchoStreamResponse(value='hello') + + +JSON +++++ +Message objects include :meth:`betterproto.Message.to_json` and +:meth:`betterproto.Message.from_json` methods for JSON (de)serialisation, and +:meth:`betterproto.Message.to_dict`, :meth:`betterproto.Message.from_dict` for +converting back and forth from JSON serializable dicts. + +For compatibility the default is to convert field names to +:attr:`betterproto.Casing.CAMEL`. You can control this behavior by passing a +different casing value, e.g: + +.. code-block:: python + + @dataclass + class MyMessage(betterproto.Message): + a_long_field_name: str = betterproto.string_field(1) + + + >>> test = MyMessage(a_long_field_name="Hello World!") + >>> test.to_dict(betterproto.Casing.SNAKE) + {"a_long_field_name": "Hello World!"} + >>> test.to_dict(betterproto.Casing.CAMEL) + {"aLongFieldName": "Hello World!"} + + >>> test.to_json(indent=2) + '{\n "aLongFieldName": "Hello World!"\n}' + + >>> test.from_dict({"aLongFieldName": "Goodbye World!"}) + >>> test.a_long_field_name + "Goodbye World!" diff --git a/docs/upgrading.md b/docs/upgrading.md deleted file mode 100644 index 84534a3..0000000 --- a/docs/upgrading.md +++ /dev/null @@ -1,16 +0,0 @@ -# Upgrade Guide - -## [1.2.5] to [2.0.0b1] - -### Updated package structures - -Generated code now strictly follows the *package structure* of the `.proto` files. -Consequently `.proto` files without a package will be combined in a single `__init__.py` file. -To avoid overwriting existing `__init__.py` files, its best to compile into a dedicated subdirectory. - -Upgrading: - -- Remove your previously compiled `.py` files. -- Create a new *empty* directory, e.g. `generated` or `lib/generated/proto` etcetera. -- Regenerate your python files into this directory -- Update import statements, e.g. `import ExampleMessage from generated` \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index c00b035..ef3189d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,11 @@ +[[package]] +category = "dev" +description = "A configurable sidebar-enabled Sphinx theme" +name = "alabaster" +optional = false +python-versions = "*" +version = "0.7.12" + [[package]] category = "main" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." @@ -43,6 +51,17 @@ docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] +[[package]] +category = "dev" +description = "Internationalization utilities" +name = "babel" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.8.0" + +[package.dependencies] +pytz = ">=2015.7" + [[package]] category = "main" description = "Backport of Python 3.7's datetime.fromisoformat" @@ -148,7 +167,7 @@ description = "Code coverage measurement for Python" name = "coverage" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.2.1" +version = "5.3" [package.extras] toml = ["toml"] @@ -182,6 +201,14 @@ optional = false python-versions = "*" version = "0.3.1" +[[package]] +category = "dev" +description = "Docutils -- Python Documentation Utilities" +name = "docutils" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "0.16" + [[package]] category = "dev" description = "A platform independent file lock." @@ -276,6 +303,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "2.10" +[[package]] +category = "dev" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +name = "imagesize" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.2.0" + [[package]] category = "dev" description = "Read metadata from Python packages" @@ -389,7 +424,7 @@ description = "Bring colors to your terminal." name = "pastel" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.2.0" +version = "0.2.1" [[package]] category = "main" @@ -453,7 +488,7 @@ description = "Pygments is a syntax highlighting package written in Python." name = "pygments" optional = false python-versions = ">=3.5" -version = "2.7.0" +version = "2.7.1" [[package]] category = "dev" @@ -532,6 +567,14 @@ pytest = ">=5.0" [package.extras] dev = ["pre-commit", "tox", "pytest-asyncio"] +[[package]] +category = "dev" +description = "World timezone definitions, modern and historical" +name = "pytz" +optional = false +python-versions = "*" +version = "2020.1" + [[package]] category = "main" description = "Alternative regular expression module, to replace re." @@ -566,6 +609,131 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" version = "1.15.0" +[[package]] +category = "dev" +description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +name = "snowballstemmer" +optional = false +python-versions = "*" +version = "2.0.0" + +[[package]] +category = "dev" +description = "Python documentation generator" +name = "sphinx" +optional = false +python-versions = ">=3.5" +version = "3.1.2" + +[package.dependencies] +Jinja2 = ">=2.3" +Pygments = ">=2.0" +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = ">=0.3.5" +docutils = ">=0.12" +imagesize = "*" +packaging = "*" +requests = ">=2.5.0" +setuptools = "*" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = "*" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = "*" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.780)", "docutils-stubs"] +test = ["pytest", "pytest-cov", "html5lib", "typed-ast", "cython"] + +[[package]] +category = "dev" +description = "Read the Docs theme for Sphinx" +name = "sphinx-rtd-theme" +optional = false +python-versions = "*" +version = "0.5.0" + +[package.dependencies] +sphinx = "*" + +[package.extras] +dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] + +[[package]] +category = "dev" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +name = "sphinxcontrib-applehelp" +optional = false +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +name = "sphinxcontrib-devhelp" +optional = false +python-versions = ">=3.5" +version = "1.0.2" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +name = "sphinxcontrib-htmlhelp" +optional = false +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +category = "dev" +description = "A sphinx extension which renders display math in HTML via JavaScript" +name = "sphinxcontrib-jsmath" +optional = false +python-versions = ">=3.5" +version = "1.0.1" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +category = "dev" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +name = "sphinxcontrib-qthelp" +optional = false +python-versions = ">=3.5" +version = "1.0.3" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +category = "dev" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +name = "sphinxcontrib-serializinghtml" +optional = false +python-versions = ">=3.5" +version = "1.1.4" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + [[package]] category = "main" description = "Python Library for Tom's Obvious, Minimal Language" @@ -680,11 +848,15 @@ testing = ["jaraco.itertools", "func-timeout"] compiler = ["black", "jinja2", "protobuf"] [metadata] -content-hash = "2a56c1e83222f20c06385fd175a00e158419e11780c14ca5153b23e1dfa3d651" +content-hash = "aa1cdf753b393b5b61a5794989b6883709d82dd13e924c1b1d92d93c21dd256c" lock-version = "1.0" python-versions = "^3.6" [metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, @@ -700,6 +872,10 @@ attrs = [ {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, ] +babel = [ + {file = "Babel-2.8.0-py2.py3-none-any.whl", hash = "sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"}, + {file = "Babel-2.8.0.tar.gz", hash = "sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38"}, +] backports-datetime-fromisoformat = [ {file = "backports-datetime-fromisoformat-1.0.0.tar.gz", hash = "sha256:9577a2a9486cd7383a5f58b23bb8e81cf0821dbbc0eb7c87d3fa198c1df40f5c"}, ] @@ -733,40 +909,40 @@ colorama = [ {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, ] coverage = [ - {file = "coverage-5.2.1-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4"}, - {file = "coverage-5.2.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01"}, - {file = "coverage-5.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8"}, - {file = "coverage-5.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59"}, - {file = "coverage-5.2.1-cp27-cp27m-win32.whl", hash = "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3"}, - {file = "coverage-5.2.1-cp27-cp27m-win_amd64.whl", hash = "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f"}, - {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd"}, - {file = "coverage-5.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651"}, - {file = "coverage-5.2.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b"}, - {file = "coverage-5.2.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d"}, - {file = "coverage-5.2.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3"}, - {file = "coverage-5.2.1-cp35-cp35m-win32.whl", hash = "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"}, - {file = "coverage-5.2.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962"}, - {file = "coverage-5.2.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082"}, - {file = "coverage-5.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716"}, - {file = "coverage-5.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb"}, - {file = "coverage-5.2.1-cp36-cp36m-win32.whl", hash = "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d"}, - {file = "coverage-5.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546"}, - {file = "coverage-5.2.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811"}, - {file = "coverage-5.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258"}, - {file = "coverage-5.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034"}, - {file = "coverage-5.2.1-cp37-cp37m-win32.whl", hash = "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46"}, - {file = "coverage-5.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8"}, - {file = "coverage-5.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0"}, - {file = "coverage-5.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd"}, - {file = "coverage-5.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b"}, - {file = "coverage-5.2.1-cp38-cp38-win32.whl", hash = "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd"}, - {file = "coverage-5.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d"}, - {file = "coverage-5.2.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3"}, - {file = "coverage-5.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4"}, - {file = "coverage-5.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4"}, - {file = "coverage-5.2.1-cp39-cp39-win32.whl", hash = "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89"}, - {file = "coverage-5.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b"}, - {file = "coverage-5.2.1.tar.gz", hash = "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b"}, + {file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"}, + {file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"}, + {file = "coverage-5.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9"}, + {file = "coverage-5.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729"}, + {file = "coverage-5.3-cp27-cp27m-win32.whl", hash = "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d"}, + {file = "coverage-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418"}, + {file = "coverage-5.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9"}, + {file = "coverage-5.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5"}, + {file = "coverage-5.3-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822"}, + {file = "coverage-5.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097"}, + {file = "coverage-5.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9"}, + {file = "coverage-5.3-cp35-cp35m-win32.whl", hash = "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636"}, + {file = "coverage-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f"}, + {file = "coverage-5.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237"}, + {file = "coverage-5.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54"}, + {file = "coverage-5.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7"}, + {file = "coverage-5.3-cp36-cp36m-win32.whl", hash = "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a"}, + {file = "coverage-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d"}, + {file = "coverage-5.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"}, + {file = "coverage-5.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f"}, + {file = "coverage-5.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c"}, + {file = "coverage-5.3-cp37-cp37m-win32.whl", hash = "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751"}, + {file = "coverage-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709"}, + {file = "coverage-5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516"}, + {file = "coverage-5.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f"}, + {file = "coverage-5.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259"}, + {file = "coverage-5.3-cp38-cp38-win32.whl", hash = "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82"}, + {file = "coverage-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221"}, + {file = "coverage-5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978"}, + {file = "coverage-5.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21"}, + {file = "coverage-5.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24"}, + {file = "coverage-5.3-cp39-cp39-win32.whl", hash = "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7"}, + {file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"}, + {file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"}, ] curtsies = [ {file = "curtsies-0.3.4-py2.py3-none-any.whl", hash = "sha256:068db8e5d8a2f23b765d648a66dfa9445cf2412177126ae946a7357ade992640"}, @@ -780,6 +956,10 @@ distlib = [ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, ] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, +] filelock = [ {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, @@ -904,6 +1084,10 @@ idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] +imagesize = [ + {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, + {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, +] importlib-metadata = [ {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, @@ -999,8 +1183,8 @@ packaging = [ {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, ] pastel = [ - {file = "pastel-0.2.0-py2.py3-none-any.whl", hash = "sha256:18b559dc3ad4ba9b8bd5baebe6503f25f36d21460f021cf27a8d889cb5d17840"}, - {file = "pastel-0.2.0.tar.gz", hash = "sha256:46155fc523bdd4efcd450bbcb3f2b94a6e3b25edc0eb493e081104ad09e1ca36"}, + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, ] pathspec = [ {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, @@ -1039,8 +1223,8 @@ py = [ {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, ] pygments = [ - {file = "Pygments-2.7.0-py3-none-any.whl", hash = "sha256:2df50d16b45b977217e02cba6c8422aaddb859f3d0570a88e09b00eafae89c6e"}, - {file = "Pygments-2.7.0.tar.gz", hash = "sha256:2594e8fdb06fef91552f86f4fd3a244d148ab24b66042036e64f29a291515048"}, + {file = "Pygments-2.7.1-py3-none-any.whl", hash = "sha256:307543fe65c0947b126e83dd5a61bd8acbd84abec11f43caebaf5534cbc17998"}, + {file = "Pygments-2.7.1.tar.gz", hash = "sha256:926c3f319eda178d1bd90851e4317e6d8cdb5e292a3386aac9bd75eca29cf9c7"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, @@ -1061,6 +1245,10 @@ pytest-mock = [ {file = "pytest-mock-3.3.1.tar.gz", hash = "sha256:a4d6d37329e4a893e77d9ffa89e838dd2b45d5dc099984cf03c703ac8411bb82"}, {file = "pytest_mock-3.3.1-py3-none-any.whl", hash = "sha256:024e405ad382646318c4281948aadf6fe1135632bea9cc67366ea0c4098ef5f2"}, ] +pytz = [ + {file = "pytz-2020.1-py2.py3-none-any.whl", hash = "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed"}, + {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, +] regex = [ {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, @@ -1092,6 +1280,42 @@ six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, ] +snowballstemmer = [ + {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, + {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, +] +sphinx = [ + {file = "Sphinx-3.1.2-py3-none-any.whl", hash = "sha256:97dbf2e31fc5684bb805104b8ad34434ed70e6c588f6896991b2fdfd2bef8c00"}, + {file = "Sphinx-3.1.2.tar.gz", hash = "sha256:b9daeb9b39aa1ffefc2809b43604109825300300b987a24f45976c001ba1a8fd"}, +] +sphinx-rtd-theme = [ + {file = "sphinx_rtd_theme-0.5.0-py2.py3-none-any.whl", hash = "sha256:373413d0f82425aaa28fb288009bf0d0964711d347763af2f1b65cafcb028c82"}, + {file = "sphinx_rtd_theme-0.5.0.tar.gz", hash = "sha256:22c795ba2832a169ca301cd0a083f7a434e09c538c70beb42782c073651b707d"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"}, + {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"}, + {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"}, +] toml = [ {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, diff --git a/pyproject.toml b/pyproject.toml index 2debf7f..4d3068a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,8 @@ pytest-asyncio = "^0.12.0" pytest-cov = "^2.9.0" pytest-mock = "^3.1.1" tox = "^3.15.1" +sphinx = "3.1.2" +sphinx-rtd-theme = "0.5.0" asv = "^0.4.2" [tool.poetry.scripts] @@ -48,6 +50,7 @@ test = { cmd = "pytest --cov src", help = "Run tests" } types = { cmd = "mypy src --ignore-missing-imports", help = "Check types with mypy" } format = { cmd = "black . --exclude tests/output_", help = "Apply black formatting to source code" } clean = { cmd = "rm -rf .coverage .mypy_cache .pytest_cache dist betterproto.egg-info **/__pycache__ tests/output_*", help = "Clean out generated files from the workspace" } +docs = { cmd = "sphinx-build docs docs/build", help = "Build the sphinx docs"} bench = { shell = "asv run master^! && asv run HEAD^! && asv compare master HEAD", help = "Benchmark current commit vs. master branch"} # CI tasks diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py index 5985798..9a46fe1 100644 --- a/src/betterproto/__init__.py +++ b/src/betterproto/__init__.py @@ -120,8 +120,8 @@ DATETIME_ZERO = datetime_default_gen() class Casing(enum.Enum): """Casing constants for serialization.""" - CAMEL = camel_case - SNAKE = snake_case + CAMEL = camel_case #: A camelCase sterilization function. + SNAKE = snake_case #: A snake_case sterilization function. PLACEHOLDER: Any = object() @@ -249,11 +249,26 @@ def map_field( class Enum(enum.IntEnum): - """Protocol buffers enumeration base class. Acts like `enum.IntEnum`.""" + """ + The base class for protobuf enumerations, all generated enumerations will inherit + from this. Bases :class:`enum.IntEnum`. + """ @classmethod - def from_string(cls, name: str) -> int: - """Return the value which corresponds to the string name.""" + def from_string(cls, name: str) -> "Enum": + """ + Return the value which corresponds to the string name. + + Parameters + ----------- + name: :class:`str` + The name of the enum member to get + + Raises + ------- + :exc:`ValueError` + The member was not found in the Enum. + """ try: return cls._member_map_[name] except KeyError as e: @@ -497,9 +512,15 @@ class ProtoClassMetadata: class Message(ABC): """ - A protobuf message base class. Generated code will inherit from this and - register the message fields which get used by the serializers and parsers - to go between Python, binary and JSON protobuf message representations. + The base class for protobuf messages, all generated messages will inherit from + this. This class registers the message fields which are used by the serializers and + parsers to go between the Python, binary and JSON representations of the message. + + .. container:: operations + + .. describe:: bytes(x) + + Calls :meth:`__bytes__`. """ _serialized_on_wire: bool @@ -605,7 +626,7 @@ class Message(ABC): def __bytes__(self) -> bytes: """ - Get the binary encoded Protobuf representation of this instance. + Get the binary encoded Protobuf representation of this message instance. """ output = bytearray() for field_name, meta in self._betterproto.meta_by_field_name.items(): @@ -684,7 +705,20 @@ class Message(ABC): return bytes(output) # For compatibility with other libraries - SerializeToString = __bytes__ + def SerializeToString(self: T) -> bytes: + """ + Get the binary encoded Protobuf representation of this message instance. + + .. note:: + This is a method for compatibility with other libraries, + you should really use ``bytes(x)``. + + Returns + -------- + :class:`bytes` + The binary encoded Protobuf representation of this message instance + """ + return bytes(self) @classmethod def _type_hint(cls, field_name: str) -> Type: @@ -788,6 +822,16 @@ class Message(ABC): """ Parse the binary encoded Protobuf into this message instance. This returns the instance itself and is therefore assignable and chainable. + + Parameters + ----------- + data: :class:`bytes` + The data to parse the protobuf from. + + Returns + -------- + :class:`Message` + The initialized message. """ # Got some data over the wire self._serialized_on_wire = True @@ -838,20 +882,47 @@ class Message(ABC): # For compatibility with other libraries. @classmethod def FromString(cls: Type[T], data: bytes) -> T: + """ + Parse the binary encoded Protobuf into this message instance. This + returns the instance itself and is therefore assignable and chainable. + + .. note:: + This is a method for compatibility with other libraries, + you should really use :meth:`parse`. + + + Parameters + ----------- + data: :class:`bytes` + The data to parse the protobuf from. + + Returns + -------- + :class:`Message` + The initialized message. + """ return cls().parse(data) def to_dict( self, casing: Casing = Casing.CAMEL, include_default_values: bool = False ) -> Dict[str, Any]: """ - Returns a dict representation of this message instance which can be - used to serialize to e.g. JSON. Defaults to camel casing for - compatibility but can be set to other modes. + Returns a JSON serializable dict representation of this object. - `include_default_values` can be set to `True` to include default - values of fields. E.g. an `int32` type field with `0` value will - not be in returned dict if `include_default_values` is set to - `False`. + Parameters + ----------- + casing: :class:`Casing` + The casing to use for key values. Default is :attr:`Casing.CAMEL` for + compatibility purposes. + include_default_values: :class:`bool` + If ``True`` will include the default values of fields. Default is ``False``. + E.g. an ``int32`` field will be included with a value of ``0`` if this is + set to ``True``, otherwise this would be ignored. + + Returns + -------- + Dict[:class:`str`, Any] + The JSON serializable dict representation of this object. """ output: Dict[str, Any] = {} field_types = self._type_hints() @@ -938,10 +1009,20 @@ class Message(ABC): output[cased_name] = value return output - def from_dict(self: T, value: dict) -> T: + def from_dict(self: T, value: Dict[str, Any]) -> T: """ - Parse the key/value pairs in `value` into this message instance. This - returns the instance itself and is therefore assignable and chainable. + Parse the key/value pairs into the current message instance. This returns the + instance itself and is therefore assignable and chainable. + + Parameters + ----------- + value: Dict[:class:`str`, Any] + The dictionary to parse from. + + Returns + -------- + :class:`Message` + The initialized message. """ self._serialized_on_wire = True for key in value: @@ -998,28 +1079,70 @@ class Message(ABC): return self def to_json(self, indent: Union[None, int, str] = None) -> str: - """Returns the encoded JSON representation of this message instance.""" + """A helper function to parse the message instance into its JSON + representation. + + This is equivalent to:: + + json.dumps(message.to_dict(), indent=indent) + + Parameters + ----------- + indent: Optional[Union[:class:`int`, :class:`str`]] + The indent to pass to :func:`json.dumps`. + + Returns + -------- + :class:`str` + The JSON representation of the message. + """ return json.dumps(self.to_dict(), indent=indent) def from_json(self: T, value: Union[str, bytes]) -> T: - """ - Parse the key/value pairs in `value` into this message instance. This - returns the instance itself and is therefore assignable and chainable. + """A helper function to return the message instance from its JSON + representation. This returns the instance itself and is therefore assignable + and chainable. + + This is equivalent to:: + + return message.from_dict(json.loads(value)) + + Parameters + ----------- + value: Union[:class:`str`, :class:`bytes`] + The value to pass to :func:`json.loads`. + + Returns + -------- + :class:`Message` + The initialized message. """ return self.from_dict(json.loads(value)) def serialized_on_wire(message: Message) -> bool: """ - True if this message was or should be serialized on the wire. This can - be used to detect presence (e.g. optional wrapper message) and is used - internally during parsing/serialization. + If this message was or should be serialized on the wire. This can be used to detect + presence (e.g. optional wrapper message) and is used internally during + parsing/serialization. + + Returns + -------- + :class:`bool` + Whether this message was or should be serialized on the wire. """ return message._serialized_on_wire def which_one_of(message: Message, group_name: str) -> Tuple[str, Any]: - """Return the name and value of a message's one-of field group.""" + """ + Return the name and value of a message's one-of field group. + + Returns + -------- + Tuple[:class:`str`, Any] + The field name and the value for that field. + """ field_name = message._group_current.get(group_name) if not field_name: return ("", None) diff --git a/src/betterproto/casing.py b/src/betterproto/casing.py index e21e7fd..cd3c344 100644 --- a/src/betterproto/casing.py +++ b/src/betterproto/casing.py @@ -21,14 +21,24 @@ def safe_snake_case(value: str) -> str: return value -def snake_case(value: str, strict: bool = True): +def snake_case(value: str, strict: bool = True) -> str: """ Join words with an underscore into lowercase and remove symbols. - @param value: value to convert - @param strict: force single underscores + + Parameters + ----------- + value: :class:`str` + The value to convert. + strict: :class:`bool` + Whether or not to force single underscores. + + Returns + -------- + :class:`str` + The value in snake_case. """ - def substitute_word(symbols, word, is_start): + def substitute_word(symbols: str, word: str, is_start: bool) -> str: if not word: return "" if strict: @@ -52,11 +62,21 @@ def snake_case(value: str, strict: bool = True): return snake -def pascal_case(value: str, strict: bool = True): +def pascal_case(value: str, strict: bool = True) -> str: """ Capitalize each word and remove symbols. - @param value: value to convert - @param strict: output only alphanumeric characters + + Parameters + ----------- + value: :class:`str` + The value to convert. + strict: :class:`bool` + Whether or not to output only alphanumeric characters. + + Returns + -------- + :class:`str` + The value in PascalCase. """ def substitute_word(symbols, word): @@ -77,14 +97,39 @@ def pascal_case(value: str, strict: bool = True): ) -def camel_case(value: str, strict: bool = True): +def camel_case(value: str, strict: bool = True) -> str: """ Capitalize all words except first and remove symbols. + + Parameters + ----------- + value: :class:`str` + The value to convert. + strict: :class:`bool` + Whether or not to output only alphanumeric characters. + + Returns + -------- + :class:`str` + The value in camelCase. """ return lowercase_first(pascal_case(value, strict=strict)) -def lowercase_first(value: str): +def lowercase_first(value: str) -> str: + """ + Lower cases the first character of the value. + + Parameters + ---------- + value: :class:`str` + The value to lower case. + + Returns + ------- + :class:`str` + The lower cased string. + """ return value[0:1].lower() + value[1:]