Python doc polish (#1757)

* Exercise doctests as a test not as a linter

* Add a contract artifact doctest, and exercise it

* Clean up linter issues

* Change asset data decoding output type

Previously, it was a TypedDict, but that was causing problems.  Sphinx
seems to be broken, such that none of the fields of the class were being
rendered into the doc.

Thinking on it further, I decided that a NamedTuple makes more sense
here anyways, since tuples are immutable and this output value isn't
something someone should ever build or modify.  And, NamedTuple is
getting its fields properly rendered by Sphinx.

* Add type annotations to JSON schemas docs

* Add doc publish metadata file for middlewares pkg

* Improve documentation

Note that none of the changes to .py files impact functionality in any
way, because the changes are restricted to "docstrings", which to the
Python interpreter are simply no-op statements.

However, one caveat to that is that much of these docstring changes DO
affect the functionality of automated test runs, because all of the code
examples (blocks beginning with `>>> `) are "doctests", which are
exercised via the test framework.

The index.rst files are the top-level templates for generating the
documentation, and the "automodule"/"autoclass"/etc statements pull in
the docstrings from the source code.

* correct package name in doc URL

* Move sra_client module into zero_ex namespace

* Add functions to encode asset data to bytes

* Fix: SRA client was deserializing orders weirdly

The generated code was transforming the order structure, from the camel
case field name format in the spec, into the snake case field name
format expected by Python convention.  With this problem in place, the
only way to take an order from a relayer and send it to a contract (for
fill, cancel, etc) was to manually transform the field names, one by
one, into a new structure.

* Fix problem with Web3/JSON order conversion utils

* doctest: maker, trade ZRX for WETH, not vice versa

* Remove redundant test

* Construct order in native Python, not JSON

Then convert it to JSON before sending it to the relayer.

* doctest: simplify asset units

* Add doctests for filling and cancelling

* Minor doctetst copy edits; whitespace

* Rename function, and add optional parameter

* Tweak docstrings on JSON conversion functions.

* Demo asset data decoding to view asset pairs

* Demo selecting an order from the order book

And have taker take it.

* Rename variable

* Abstract ganache from examples

Doing that exposed excessive use of the verbose
NETWORK_TO_ADDRESSES[NetworkId.Ganache] construct, so simplified that,
which ripped into eliminating other temporary variables that had been
used to hold specific contract addresses.

Also cleaned up some misplaced import statements.

* Add missing SRA client doc publication metadata

* Ran prettier on new SRA client doc pub metadata

* Remove local env customizations in doc metadata

* Eliminate temporary variable

* Rename variable

* Show `pip install` in every package's doc

* Doc NetorkID & pagination params as int, not float

* Clean up unmatched parenthesis in docs
This commit is contained in:
F. Eugene Aumson
2019-04-30 07:44:51 -04:00
committed by GitHub
parent b896f82282
commit 0564ac1530
61 changed files with 1174 additions and 965 deletions

View File

@@ -181,6 +181,7 @@ setup(
"black",
"coverage",
"coveralls",
"deprecated",
"mypy",
"mypy_extensions",
"pycodestyle",

View File

@@ -21,12 +21,14 @@ zero_ex.order_utils.asset_data_utils
:members:
.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC20AssetData
See source for class properties. Sphinx is having problems generating docs for ``TypedDict`` declarations; pull requests welcome.
:members:
:undoc-members:
:show-inheritance:
.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC721AssetData
See source for class properties. Sphinx is having problems generating docs for ``TypedDict`` declarations; pull requests welcome.
:members:
:undoc-members:
:show-inheritance:
Indices and tables
==================

View File

@@ -10,8 +10,8 @@ from typing import Any, List
from mypy_extensions import TypedDict
from web3 import Web3
from eth_abi import encode_abi
from web3 import Web3
from .type_assertions import assert_is_string, assert_is_list

View File

@@ -1,5 +1,12 @@
"""Order utilities for 0x applications.
Setup
-----
Install the package with pip::
pip install 0x-order-utils
Some methods require the caller to pass in a `Web3.BaseProvider`:code: object.
For local testing one may construct such a provider pointing at an instance of
`ganache-cli <https://www.npmjs.com/package/ganache-cli>`_ which has the 0x
@@ -7,52 +14,51 @@ contracts deployed on it. For convenience, a docker container is provided for
just this purpose. To start it:
`docker run -d -p 8545:8545 0xorg/ganache-cli:2.2.2`:code:.
Creating a 0x Order
--------------------
Constructing an order
---------------------
Here is a short demonstration on how to create a 0x order.
>>> import pprint
>>> from zero_ex.contract_addresses import (
... NETWORK_TO_ADDRESSES, NetworkId)
>>> from zero_ex.contract_addresses import NETWORK_TO_ADDRESSES, NetworkId
>>> from zero_ex.order_utils import asset_data_utils, Order
>>> NULL_ADDRESS = "0x0000000000000000000000000000000000000000"
>>> from datetime import datetime, timedelta
>>> import random
>>> my_address = "0x5409ed021d9299bf6814279a6a1411a7e866a631"
>>> exchange_address = NETWORK_TO_ADDRESSES[NetworkId.MAINNET].exchange
>>> weth_address = NETWORK_TO_ADDRESSES[NetworkId.MAINNET].ether_token
>>> zrx_address = NETWORK_TO_ADDRESSES[NetworkId.MAINNET].zrx_token
>>> maker_asset_data = (
... asset_data_utils.encode_erc20_asset_data(weth_address))
>>> taker_asset_data = (
... asset_data_utils.encode_erc20_asset_data(zrx_address))
>>> example_order: Order = {
... "makerAddress": my_address,
... "takerAddress": NULL_ADDRESS,
... "exchangeAddress": exchange_address,
... "senderAddress": NULL_ADDRESS,
... "feeRecipientAddress": NULL_ADDRESS,
... "makerAssetData": maker_asset_data,
... "takerAssetData": taker_asset_data,
... "salt": 123456789,
... "makerFee": 0,
... "takerFee": 0,
... "makerAssetAmount": 1 * 10 ** 18, # Converting token amount to base unit with 18 decimals
... "takerAssetAmount": 500 * 10 ** 18, # Converting token amount to base unit with 18 decimals
... "expirationTimeSeconds": 1553553429,
... }
>>> example_order = Order(
... makerAddress=my_address,
... takerAddress="0x0000000000000000000000000000000000000000",
... exchangeAddress=NETWORK_TO_ADDRESSES[NetworkId.MAINNET].exchange,
... senderAddress="0x0000000000000000000000000000000000000000",
... feeRecipientAddress="0x0000000000000000000000000000000000000000",
... makerAssetData=asset_data_utils.encode_erc20(
... NETWORK_TO_ADDRESSES[NetworkId.MAINNET].ether_token
... ),
... takerAssetData=asset_data_utils.encode_erc20(
... NETWORK_TO_ADDRESSES[NetworkId.MAINNET].zrx_token
... ),
... salt=random.randint(1, 100000000000000000),
... makerFee=0,
... takerFee=0,
... makerAssetAmount=1 * 10 ** 18, # Convert token amount to base unit with 18 decimals
... takerAssetAmount=500 * 10 ** 18, # Convert token amount to base unit with 18 decimals
... expirationTimeSeconds=round(
... (datetime.utcnow() + timedelta(days=1)).timestamp()
... )
... )
>>> import pprint
>>> pprint.pprint(example_order)
{'exchangeAddress': '0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
'expirationTimeSeconds': 1553553429,
{'exchangeAddress': '0x...',
'expirationTimeSeconds': ...,
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x5409ed021d9299bf6814279a6a1411a7e866a631',
'makerAddress': '0x...',
'makerAssetAmount': 1000000000000000000,
'makerAssetData': '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
'makerAssetData': b...,
'makerFee': 0,
'salt': 123456789,
'salt': ...,
'senderAddress': '0x0000000000000000000000000000000000000000',
'takerAddress': '0x0000000000000000000000000000000000000000',
'takerAssetAmount': 500000000000000000000,
'takerAssetData': '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498',
'takerAssetData': b...,
'takerFee': 0}
""" # noqa E501
@@ -128,7 +134,29 @@ class _Constants:
class Order(TypedDict): # pylint: disable=too-many-instance-attributes
"""A Web3-compatible representation of the Exchange.Order struct."""
"""A Web3-compatible representation of the Exchange.Order struct.
>>> from zero_ex.order_utils import asset_data_utils
>>> from eth_utils import remove_0x_prefix
>>> from datetime import datetime, timedelta
>>> import random
>>> order = Order(
... makerAddress=maker_address,
... takerAddress='0x0000000000000000000000000000000000000000',
... senderAddress='0x0000000000000000000000000000000000000000',
... feeRecipientAddress='0x0000000000000000000000000000000000000000',
... makerAssetData=asset_data_utils.encode_erc20(zrx_address),
... takerAssetData=asset_data_utils.encode_erc20(weth_address),
... salt=random.randint(1, 100000000000000000),
... makerFee=0,
... takerFee=0,
... makerAssetAmount=1,
... takerAssetAmount=1,
... expirationTimeSeconds=round(
... (datetime.utcnow() + timedelta(days=1)).timestamp()
... )
... )
"""
makerAddress: str
"""Address that created the order."""
@@ -203,11 +231,14 @@ def make_empty_order() -> Order:
def order_to_jsdict(
order: Order, exchange_address="0x0000000000000000000000000000000000000000"
order: Order,
exchange_address="0x0000000000000000000000000000000000000000",
signature: str = None,
) -> dict:
"""Convert a Web3-compatible order struct to a JSON-schema-compatible dict.
More specifically, do explicit decoding for the `bytes`:code: fields.
More specifically, do explicit decoding for the `bytes`:code: fields, and
convert numerics to strings.
>>> import pprint
>>> pprint.pprint(order_to_jsdict(
@@ -228,18 +259,18 @@ def order_to_jsdict(
... },
... ))
{'exchangeAddress': '0x0000000000000000000000000000000000000000',
'expirationTimeSeconds': 1,
'expirationTimeSeconds': '1',
'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x0000000000000000000000000000000000000000',
'makerAssetAmount': 1,
'makerAssetAmount': '1',
'makerAssetData': '0x0000000000000000000000000000000000000000',
'makerFee': 0,
'salt': 1,
'makerFee': '0',
'salt': '1',
'senderAddress': '0x0000000000000000000000000000000000000000',
'takerAddress': '0x0000000000000000000000000000000000000000',
'takerAssetAmount': 1,
'takerAssetAmount': '1',
'takerAssetData': '0x0000000000000000000000000000000000000000',
'takerFee': 0}
'takerFee': '0'}
"""
jsdict = cast(Dict, copy(order))
@@ -249,29 +280,43 @@ def order_to_jsdict(
jsdict["exchangeAddress"] = exchange_address
jsdict["expirationTimeSeconds"] = str(order["expirationTimeSeconds"])
jsdict["makerAssetAmount"] = str(order["makerAssetAmount"])
jsdict["takerAssetAmount"] = str(order["takerAssetAmount"])
jsdict["makerFee"] = str(order["makerFee"])
jsdict["takerFee"] = str(order["takerFee"])
jsdict["salt"] = str(order["salt"])
if signature is not None:
jsdict["signature"] = signature
assert_valid(jsdict, "/orderSchema")
return jsdict
def jsdict_order_to_struct(jsdict: dict) -> Order:
def jsdict_to_order(jsdict: dict) -> Order:
r"""Convert a JSON-schema-compatible dict order to a Web3-compatible struct.
More specifically, do explicit encoding of the `bytes`:code: fields.
More specifically, do explicit encoding of the `bytes`:code: fields, and
parse integers from strings.
>>> import pprint
>>> pprint.pprint(jsdict_order_to_struct(
>>> pprint.pprint(jsdict_to_order(
... {
... 'makerAddress': "0x0000000000000000000000000000000000000000",
... 'takerAddress': "0x0000000000000000000000000000000000000000",
... 'feeRecipientAddress': "0x0000000000000000000000000000000000000000",
... 'senderAddress': "0x0000000000000000000000000000000000000000",
... 'makerAssetAmount': 1000000000000000000,
... 'takerAssetAmount': 1000000000000000000,
... 'makerFee': 0,
... 'takerFee': 0,
... 'expirationTimeSeconds': 12345,
... 'salt': 12345,
... 'makerAssetAmount': "1000000000000000000",
... 'takerAssetAmount': "1000000000000000000",
... 'makerFee': "0",
... 'takerFee': "0",
... 'expirationTimeSeconds': "12345",
... 'salt': "12345",
... 'makerAssetData': "0x0000000000000000000000000000000000000000",
... 'takerAssetData': "0x0000000000000000000000000000000000000000",
... 'exchangeAddress': "0x0000000000000000000000000000000000000000",
@@ -303,6 +348,16 @@ def jsdict_order_to_struct(jsdict: dict) -> Order:
remove_0x_prefix(jsdict["takerAssetData"])
)
order["makerAssetAmount"] = int(jsdict["makerAssetAmount"])
order["takerAssetAmount"] = int(jsdict["takerAssetAmount"])
order["makerFee"] = int(jsdict["makerFee"])
order["takerFee"] = int(jsdict["takerFee"])
order["expirationTimeSeconds"] = int(jsdict["expirationTimeSeconds"])
order["salt"] = int(jsdict["salt"])
del order["exchangeAddress"] # type: ignore
# silence mypy pending release of
# https://github.com/python/mypy/issues/3550

View File

@@ -1,8 +1,9 @@
"""Asset data encoding and decoding utilities."""
from mypy_extensions import TypedDict
from typing import NamedTuple
import eth_abi
from deprecated.sphinx import deprecated
from zero_ex.dev_utils import abi_utils
from zero_ex.dev_utils.type_assertions import assert_is_string, assert_is_int
@@ -13,23 +14,30 @@ ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH = 53
SELECTOR_LENGTH = 10
class ERC20AssetData(TypedDict):
class ERC20AssetData(NamedTuple):
"""Object interface to ERC20 asset data."""
asset_proxy_id: str
"""asset proxy id"""
"""Asset proxy identifier."""
token_address: str
"""Token address"""
class ERC721AssetData(TypedDict):
class ERC721AssetData(NamedTuple):
"""Object interface to ERC721 asset data."""
asset_proxy_id: str
"""Asset proxy identifier."""
token_address: str
"""Token address"""
token_id: int
"""Token identifier."""
@deprecated(reason='use `"0x"+encode_erc20().hex()` instead')
def encode_erc20_asset_data(token_address: str) -> str:
"""Encode an ERC20 token address into an asset data string.
@@ -48,13 +56,28 @@ def encode_erc20_asset_data(token_address: str) -> str:
)
def encode_erc20(token_address: str) -> bytes:
"""Encode an ERC20 token address into asset data bytes.
:param token_address: the ERC20 token's contract address.
:returns: hex encoded asset data string, usable in the makerAssetData or
takerAssetData fields in a 0x order.
>>> encode_erc20('0x1dc4c1cefef38a777b15aa20260a54e584b16c48').hex()
'f47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48'
"""
assert_is_string(token_address, "token_address")
return abi_utils.simple_encode("ERC20Token(address)", token_address)
def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
"""Decode an ERC20 asset data hex string.
:param asset_data: String produced by prior call to encode_erc20_asset_data()
>>> decode_erc20_asset_data("0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48")
{'asset_proxy_id': '0xf47261b0', 'token_address': '0x1dc4c1cefef38a777b15aa20260a54e584b16c48'}
ERC20AssetData(asset_proxy_id='0xf47261b0', token_address='0x1dc4c1cefef38a777b15aa20260a54e584b16c48')
""" # noqa: E501 (line too long)
assert_is_string(asset_data, "asset_data")
@@ -79,9 +102,12 @@ def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData:
["address"], bytes.fromhex(asset_data[SELECTOR_LENGTH:])
)[0]
return {"asset_proxy_id": asset_proxy_id, "token_address": token_address}
return ERC20AssetData(
asset_proxy_id=asset_proxy_id, token_address=token_address
)
@deprecated(reason='use `"0x"+encode_erc721().hex()` instead')
def encode_erc721_asset_data(token_address: str, token_id: int) -> str:
"""Encode an ERC721 asset data hex string.
@@ -104,11 +130,30 @@ def encode_erc721_asset_data(token_address: str, token_id: int) -> str:
)
def encode_erc721(token_address: str, token_id: int) -> bytes:
"""Encode an ERC721 token address into asset data bytes.
:param token_address: the ERC721 token's contract address.
:param token_id: the identifier of the asset's instance of the token.
:returns: hex encoded asset data string, usable in the makerAssetData or
takerAssetData fields in a 0x order.
>>> encode_erc721('0x1dc4c1cefef38a777b15aa20260a54e584b16c48', 1).hex()
'025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001'
""" # noqa: E501 (line too long)
assert_is_string(token_address, "token_address")
assert_is_int(token_id, "token_id")
return abi_utils.simple_encode(
"ERC721Token(address,uint256)", token_address, token_id
)
def decode_erc721_asset_data(asset_data: str) -> ERC721AssetData:
"""Decode an ERC721 asset data hex string.
>>> decode_erc721_asset_data('0x025717920000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c480000000000000000000000000000000000000000000000000000000000000001')
{'asset_proxy_id': '0x02571792', 'token_address': '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', 'token_id': 1}
ERC721AssetData(asset_proxy_id='0x02571792', token_address='0x1dc4c1cefef38a777b15aa20260a54e584b16c48', token_id=1)
""" # noqa: E501 (line too long)
assert_is_string(asset_data, "asset_data")
@@ -134,8 +179,8 @@ def decode_erc721_asset_data(asset_data: str) -> ERC721AssetData:
["address", "uint256"], bytes.fromhex(asset_data[SELECTOR_LENGTH:])
)
return {
"asset_proxy_id": asset_proxy_id,
"token_address": token_address,
"token_id": token_id,
}
return ERC721AssetData(
asset_proxy_id=asset_proxy_id,
token_address=token_address,
token_id=token_id,
)

View File

@@ -0,0 +1,2 @@
def deprecated(*args, **kwargs):
...