Skip to content

Commit

Permalink
feat!: upgrade to pydantic v2 (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Sep 22, 2023
1 parent 4703f3f commit f041671
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 62 deletions.
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
rev: v4.4.0
hooks:
- id: check-yaml

Expand All @@ -10,24 +10,24 @@ repos:
- id: isort

- repo: https://github.com/psf/black
rev: 23.3.0
rev: 23.9.1
hooks:
- id: black
name: black

- repo: https://github.com/pycqa/flake8
rev: 6.0.0
rev: 6.1.0
hooks:
- id: flake8

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.991
rev: v1.5.1
hooks:
- id: mypy
additional_dependencies: [types-setuptools, types-requests]

- repo: https://github.com/executablebooks/mdformat
rev: 0.7.14
rev: 0.7.17
hooks:
- id: mdformat
additional_dependencies: [mdformat-gfm, mdformat-frontmatter]
Expand Down
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Dependencies

- [python3](https://www.python.org/downloads/release/python-368/) version 3.7.2 or greater, python3-dev
- [python3](https://www.python.org/downloads/release/python-368/) version 3.8 or greater, python3-dev

## Installation

Expand Down Expand Up @@ -40,12 +40,6 @@ python3 setup.py install
['1inch']
```

## Development

This project is in early development and should be considered an alpha.
Things might not work, breaking changes are likely.
Comments, questions, criticisms and pull requests are welcomed.

## License

This project is licensed under the [MIT license](LICENSE).
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools>=51.1.1", "wheel", "setuptools_scm[toml]>=5.0"]
requires = ["setuptools>=51.1.1", "wheel", "setuptools_scm[toml]>=5.0,<8"]

[tool.mypy]
exclude = "build/"
Expand All @@ -19,7 +19,6 @@ include = '\.pyi?$'

[tool.pytest.ini_options]
addopts = """
-n auto
-p no:ape_test
--cov-branch
--cov-report term
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ exclude =
.tox
docs
build
./tokenlists/version.py
18 changes: 9 additions & 9 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import find_packages, setup # type: ignore
from setuptools import find_packages, setup

extras_require = {
"test": [ # `test` GitHub Action jobs uses this
"pytest>=6.0", # Core testing package
"pytest-xdist", # multi-process runner
"pytest-cov", # Coverage analyzer plugin
"hypothesis>=6.2.0,<7", # Strategy-based fuzzer
"hypothesis>=6.86.2,<7", # Strategy-based fuzzer
"PyGithub>=1.54,<2", # Necessary to pull official schema from github
"hypothesis-jsonschema==0.19.0", # Fuzzes based on a json schema
],
"lint": [
"black>=23.3.0,<24", # auto-formatter and linter
"mypy>=0.991,<1", # Static type analyzer
"black>=23.9.1,<24", # auto-formatter and linter
"mypy>=1.5.1,<2", # Static type analyzer
"types-requests", # Needed due to mypy typeshed
"flake8>=6.0.0,<7", # Style linter
"types-setuptools", # Needed due to mypy typeshed
"flake8>=6.1.0,<7", # Style linter
"isort>=5.10.1", # Import sorting linter
"mdformat>=0.7.16", # Auto-formatter for markdown
"mdformat>=0.7.17", # Auto-formatter for markdown
"mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown
"mdformat-frontmatter>=0.4.1", # Needed for frontmatters-style headers in issue templates
],
Expand Down Expand Up @@ -68,9 +69,8 @@
install_requires=[
"importlib-metadata ; python_version<'3.8'",
"click>=8.1.3,<9",
"pydantic>=1.9.2,<2",
"pydantic>=2.3.0,<3",
"pyyaml>=6.0,<7",
"semantic-version>=2.10.0,<3",
"requests>=2.28.1,<3",
],
entry_points={"console_scripts": ["tokenlists=tokenlists._cli:cli"]},
Expand All @@ -82,7 +82,7 @@
packages=find_packages(exclude=["tests", "tests.*"]),
package_data={"ape_tokens": ["py.typed"]},
classifiers=[
"Development Status :: 3 - Alpha",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Natural Language :: English",
Expand Down
6 changes: 4 additions & 2 deletions tests/functional/test_schema_fuzzing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pytest
import requests # type: ignore[import]
import requests
from hypothesis import HealthCheck, given, settings
from hypothesis_jsonschema import from_schema
from pydantic import ValidationError
Expand Down Expand Up @@ -27,7 +27,9 @@ def clean_data(tl: dict) -> dict:
@settings(suppress_health_check=(HealthCheck.too_slow,))
def test_schema(token_list):
try:
assert TokenList.parse_obj(token_list).dict() == clean_data(token_list)
assert TokenList.model_validate(token_list).model_dump(mode="json") == clean_data(
token_list
)

except (ValidationError, ValueError):
pass # Expect these kinds of errors
50 changes: 44 additions & 6 deletions tests/functional/test_uniswap_examples.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import os
from typing import Any, Optional

import github
import pytest
import requests # type: ignore[import]
import requests
from pydantic import ValidationError

from tokenlists import TokenList
Expand All @@ -27,9 +28,46 @@ def test_uniswap_tokenlists(token_list_name):
# https://github.com/Uniswap/token-lists/pull/420
token_list.pop("tokenMap")

if "invalid" not in token_list_name:
assert TokenList.parse_obj(token_list).dict() == token_list

else:
if "invalid" in token_list_name:
with pytest.raises((ValidationError, ValueError)):
TokenList.parse_obj(token_list).dict()
TokenList.model_validate(token_list)
else:
actual = TokenList.model_validate(token_list).model_dump(mode="json")

def assert_tokenlists(_actual: Any, _expected: Any, parent_key: Optional[str] = None):
parent_key = parent_key or "__root__"
assert type(_actual) is type(_expected)

if isinstance(_actual, list):
for idx, (actual_item, expected_item) in enumerate(zip(_actual, _expected)):
assert_tokenlists(
actual_item, expected_item, parent_key=f"{parent_key}_index_{idx}"
)

elif isinstance(_actual, dict):
unexpected = {}
handled = set()
for key, actual_value in _actual.items():
if key not in _expected:
unexpected[key] = actual_value
continue

expected_value = _expected[key]
assert type(actual_value) is type(expected_value)
assert_tokenlists(actual_value, expected_value, parent_key=key)
handled.add(key)

handled_str = ", ".join(list(handled)) or "<Nothing handled>"
missing = {f"{x}" for x in _expected if x not in handled}
unexpected_str = ", ".join([f"{k}={v}" for k, v in unexpected.items()])
assert not unexpected, f"Unexpected keys: {unexpected_str}, Parent: {parent_key}"
assert not missing, (
f"Missing keys: '{', '.join(list(missing))}'; "
f"handled: '{handled_str}', "
f"Parent: {parent_key}."
)

else:
assert _actual == _expected

assert_tokenlists(actual, token_list)
2 changes: 1 addition & 1 deletion tests/integration/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_install(runner, cli):
assert "No tokenlists exist" in result.output

result = runner.invoke(cli, ["install", TEST_URI])
assert result.exit_code == 0
assert result.exit_code == 0, result.output

result = runner.invoke(cli, ["list"])
assert result.exit_code == 0
Expand Down
4 changes: 2 additions & 2 deletions tokenlists/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def list_tokens(search, tokenlist_name, chain_id):
lambda t: pattern.match(t.symbol),
manager.get_tokens(tokenlist_name, chain_id),
):
click.echo("{address} ({symbol})".format(**token_info.dict()))
click.echo("{address} ({symbol})".format(**token_info.model_dump(mode="json")))


@cli.command(short_help="Display the info for a particular token")
Expand All @@ -92,7 +92,7 @@ def token_info(symbol, tokenlist_name, chain_id, case_insensitive):
raise click.ClickException("No tokenlists available!")

token_info = manager.get_token_info(symbol, tokenlist_name, chain_id, case_insensitive)
token_info = token_info.dict()
token_info = token_info.model_dump(mode="json")

if "tags" not in token_info:
token_info["tags"] = ""
Expand Down
12 changes: 9 additions & 3 deletions tokenlists/manager.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from json import JSONDecodeError
from typing import Iterator, List, Optional

import requests
Expand All @@ -15,7 +16,7 @@ def __init__(self):
# Load all the ones cached on disk
self.installed_tokenlists = {}
for path in self.cache_folder.glob("*.json"):
tokenlist = TokenList.parse_file(path)
tokenlist = TokenList.model_validate_json(path.read_text())
self.installed_tokenlists[tokenlist.name] = tokenlist

self.default_tokenlist = config.DEFAULT_TOKENLIST
Expand All @@ -42,13 +43,18 @@ def install_tokenlist(self, uri: str) -> str:
# Load and store the tokenlist
response = requests.get(uri)
response.raise_for_status()
tokenlist = TokenList.parse_obj(response.json())
try:
response_json = response.json()
except JSONDecodeError as err:
raise ValueError(f"Invalid response: {response.text}") from err

tokenlist = TokenList.model_validate(response_json)
self.installed_tokenlists[tokenlist.name] = tokenlist

# Cache it on disk for later instances
self.cache_folder.mkdir(exist_ok=True)
token_list_file = self.cache_folder.joinpath(f"{tokenlist.name}.json")
token_list_file.write_text(tokenlist.json())
token_list_file.write_text(tokenlist.model_dump_json())

return tokenlist.name

Expand Down
Loading

0 comments on commit f041671

Please sign in to comment.