From 0dc489fe8d52ae14860fd8d89f184d45ed6cc245 Mon Sep 17 00:00:00 2001 From: Aedial Date: Sat, 29 Apr 2023 15:42:13 +0200 Subject: [PATCH] [DOCS][TEST] Expand docs to tests and fix some tests --- README.md | 4 +- docs/requirements.txt | 7 - docs/source/conf.py | 33 ++- docs/source/index.rst | 9 + docs/source/tests/api/api.boilerplate.rst | 7 + docs/source/tests/api/api.rst | 47 ++++ novelai_api/__init__.py | 6 + noxfile.py | 7 +- pyproject.toml | 12 + tests/api/boilerplate.py | 8 + tests/api/conftest.py | 16 +- .../test_decrypt_encrypt_integrity_check.py | 223 +++++++----------- tests/api/test_imagegen_samplers.py | 7 - tests/api/test_sync_gen.py | 3 - tests/api/test_textgen_presets.py | 3 - tests/api/test_textgen_sanity.py | 5 +- 16 files changed, 217 insertions(+), 180 deletions(-) delete mode 100644 docs/requirements.txt create mode 100644 docs/source/tests/api/api.boilerplate.rst create mode 100644 docs/source/tests/api/api.rst diff --git a/README.md b/README.md index f93e1d7..fe70713 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,14 @@ Download via [pip](https://pypi.org/project/novelai-api): pip install novelai-api ``` -A full list of examples is available in the [example](/example) directory +A full list of examples is available in the [example](example) directory The API works through the NovelAIAPI object. It is split in 2 groups: NovelAIAPI.low_level and NovelAIAPI.high_level ## low_level The low level interface is a strict implementation of the official API (). -It only checks for input types via assert and output schema if NovelAIAPI.low_level.is_schema_validation_enabled is True +It only checks for input types via assert, and output schema if NovelAIAPI.low_level.is_schema_validation_enabled is True ## high_level The high level interface builds on the low level one for easier handling of complex settings. diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index f1b5af7..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -sphinx==6.1.3 -# patched repo to work with relative links -git+https://github.com/Aedial/MyST-Parser -linkify-it-py -sphinx-copybutton -sphinx_last_updated_by_git -sphinx-hoverxref diff --git a/docs/source/conf.py b/docs/source/conf.py index 0a8c4ff..f402d3e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -8,8 +8,8 @@ import os import sys from pathlib import Path -from types import ModuleType -from typing import List +from types import FunctionType +from typing import List, Union from sphinx.application import Sphinx from sphinx.ext.autodoc import Options @@ -32,6 +32,7 @@ extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", "sphinx.ext.extlinks", "sphinx.ext.viewcode", "myst_parser", @@ -40,6 +41,8 @@ "hoverxref.extension", ] +add_module_names = False + autodoc_class_signature = "separated" autodoc_member_order = "bysource" autodoc_typehints_format = "fully-qualified" @@ -81,7 +84,11 @@ # -- Hooks ------------------------------------------------------------------- -def format_docstring(_app: Sphinx, what: str, name: str, obj: ModuleType, _options: Options, lines: List[str]): +def format_docstring(_app: Sphinx, what: str, name: str, obj, _options: Options, lines: List[str]): + """ + Inject metadata in docstrings if necessary + """ + kwargs = { "obj_type": what, "obj_name": name, @@ -99,5 +106,25 @@ def format_docstring(_app: Sphinx, what: str, name: str, obj: ModuleType, _optio lines[i] = line.format(**kwargs) +def hide_test_signature( + _app: Sphinx, + what: str, + name: str, + _obj: FunctionType, + _options: Options, + signature: str, + return_annotation: Union[str, None], +): + if what == "function": + module_name, *_, file_name, _func_name = name.split(".") + + # erase signature for functions from test files + if module_name == "tests" and file_name.startswith("test_"): + return "", None + + return signature, return_annotation + + def setup(app): app.connect("autodoc-process-docstring", format_docstring) + app.connect("autodoc-process-signature", hide_test_signature) diff --git a/docs/source/index.rst b/docs/source/index.rst index cbd9ab9..8999156 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -31,3 +31,12 @@ example :maxdepth: 2 example/example + + +API +--- + +.. toctree:: + :maxdepth: 3 + + tests/api/api diff --git a/docs/source/tests/api/api.boilerplate.rst b/docs/source/tests/api/api.boilerplate.rst new file mode 100644 index 0000000..5634568 --- /dev/null +++ b/docs/source/tests/api/api.boilerplate.rst @@ -0,0 +1,7 @@ +boilerplate +=========== + +.. automodule:: tests.api.boilerplate + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/tests/api/api.rst b/docs/source/tests/api/api.rst new file mode 100644 index 0000000..aaa0798 --- /dev/null +++ b/docs/source/tests/api/api.rst @@ -0,0 +1,47 @@ +API directory +============= + +Requirements +------------ + + +Usage +----- + + +Content +------- + +test_decrypt_encrypt_integrity_check.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. automodule:: tests.api.test_decrypt_encrypt_integrity_check + :members: + +test_imagegen_samplers.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. automodule:: tests.api.test_imagegen_samplers + :members: + +test_sync_gen.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. automodule:: tests.api.test_sync_gen + :members: + +test_textgen_presets.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. automodule:: tests.api.test_textgen_presets + :members: + +test_textgen_sanity.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. automodule:: tests.api.test_textgen_sanity + :members: + + +Reference +--------- + +.. toctree:: + :maxdepth: 2 + + api.boilerplate diff --git a/novelai_api/__init__.py b/novelai_api/__init__.py index 8f22986..9e810b7 100644 --- a/novelai_api/__init__.py +++ b/novelai_api/__init__.py @@ -1,2 +1,8 @@ +""" +:class:`NovelAI_API` + +:class:`NovelAIError` +""" + from novelai_api.NovelAI_API import NovelAIAPI from novelai_api.NovelAIError import NovelAIError diff --git a/noxfile.py b/noxfile.py index 87b2321..996ef2c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -27,7 +27,7 @@ def get_dotenv(session: nox.Session): return json.loads(dotenv_str) -def install_package(session: nox.Session, *packages: str, dev: bool = False): +def install_package(session: nox.Session, *packages: str, dev: bool = False, docs: bool = False): session.install("poetry") session.install("python-dotenv") @@ -40,6 +40,8 @@ def install_package(session: nox.Session, *packages: str, dev: bool = False): poetry_groups = [] if dev: poetry_groups.extend(["--with", "dev"]) + if docs: + poetry_groups.extend(["--with", "docs"]) session.run("python", "-m", "poetry", "export", "--output=requirements.txt", "--without-hashes", *poetry_groups) session.run("python", "-m", "poetry", "build", "--format=wheel") @@ -99,8 +101,7 @@ def run(session: nox.Session): def build_docs(session: nox.Session): docs_path = pathlib.Path(__file__).parent / "docs" - install_package(session) - session.install("-r", str(docs_path / "requirements.txt")) + install_package(session, dev=True, docs=True) with session.chdir(docs_path): session.run("make", "html", external=True) diff --git a/pyproject.toml b/pyproject.toml index 782e2c3..2f23807 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,9 @@ classifiers = [ ] packages = [{include = "novelai_api"}] +[tool.poetry.urls] +"Bug Tracker" = "https://github.com/Aedial/novelai-api/issues" + [tool.poetry.dependencies] python = ">=3.7.2,<3.12" aiohttp = {extras = ["speedups"], version = "^3.8.3"} @@ -31,6 +34,15 @@ pytest-asyncio = "^0.20.1" pytest-randomly = "^3.12.0" pylint = "^2.15.5" +[tool.poetry.group.docs.dependencies] +sphinx = "^5.3.0" +# patched repo to work with relative links +myst_parser = {git = "https://github.com/Aedial/MyST-Parser", rev = "adcdb9a"} +linkify-it-py = "^2.0.0" +sphinx-copybutton = "^0.5.2" +sphinx-last-updated-by-git = "^0.3.4" +sphinx-hoverxref = "^1.3.0" + [tool.flake8] # TODO: add flake when supports come (https://github.com/PyCQA/flake8/issues/234) diff --git a/tests/api/boilerplate.py b/tests/api/boilerplate.py index 37cc9cb..492524d 100644 --- a/tests/api/boilerplate.py +++ b/tests/api/boilerplate.py @@ -124,6 +124,10 @@ async def wrap(*args, **kwargs): class JSONEncoder(json.JSONEncoder): + """ + Extended JSON encoder to support bytes + """ + def default(self, o: Any) -> Any: if isinstance(o, bytes): return o.hex() @@ -132,6 +136,10 @@ def default(self, o: Any) -> Any: def dumps(e: Any) -> str: + """ + Shortcut to a configuration of json.dumps for consistency + """ + return json.dumps(e, indent=4, ensure_ascii=False, cls=JSONEncoder) diff --git a/tests/api/conftest.py b/tests/api/conftest.py index e560eeb..85eb5ed 100644 --- a/tests/api/conftest.py +++ b/tests/api/conftest.py @@ -13,15 +13,13 @@ def pytest_terminal_summary(terminalreporter): terminalreporter.write_sep("=", "XFAIL summary info", cyan=True, bold=True) for rep in xfailed: - reason = getattr(rep, "wasxfail", "") - terminalreporter.write("XFAIL", yellow=True) - terminalreporter.write(f" {rep.nodeid} - {reason}\n") + if not rep.failed: + reason = getattr(rep, "wasxfail", "") + terminalreporter.write("XFAIL", yellow=True) + terminalreporter.write(f" {rep.nodeid} - {reason}\n") - rep.longrepr.toterminal(terminalreporter._tw) - terminalreporter.line("") - - -# TODO: add html reporting + rep.longrepr.toterminal(terminalreporter._tw) + terminalreporter.line("") # cannot put in boilerplate because pytest is a mess @@ -31,7 +29,7 @@ def event_loop(): yield loop # clean any remaining task to avoid the warning about pending tasks - tasks = asyncio.Task.all_tasks(loop) if hasattr(asyncio.Task, "all_tasks") else asyncio.all_tasks(loop) + tasks = asyncio.all_tasks(loop) for task in tasks: # print(f"Cancelling task {task}") task.cancel() diff --git a/tests/api/test_decrypt_encrypt_integrity_check.py b/tests/api/test_decrypt_encrypt_integrity_check.py index 8583cc0..e313a18 100644 --- a/tests/api/test_decrypt_encrypt_integrity_check.py +++ b/tests/api/test_decrypt_encrypt_integrity_check.py @@ -1,19 +1,15 @@ +""" +Test if the content decryption/decompression is consistent with encryption/compression for downloaded content +""" + from asyncio import run -from os import environ as env from pathlib import Path from subprocess import PIPE, Popen from typing import Any, List -from aiohttp import ClientSession - -from novelai_api import NovelAIAPI, utils -from novelai_api.utils import ( - compress_user_data, - decompress_user_data, - decrypt_user_data, - encrypt_user_data, - get_encryption_key, -) +from novelai_api import utils +from novelai_api.utils import compress_user_data, decompress_user_data, decrypt_user_data, encrypt_user_data +from tests.api.boilerplate import API, api_handle, api_handle_sync, error_handler # noqa: F401 # pylint: disable=W0611 def compare_in_out(type_name: str, items_in: List[Any], items_out: List[Any]) -> bool: @@ -39,21 +35,15 @@ def inflate_js(data: bytes, _) -> bytes: return out -if "NAI_USERNAME" not in env or "NAI_PASSWORD" not in env: - raise RuntimeError("Please ensure that NAI_USERNAME and NAI_PASSWORD are set in your environment") - -username = env["NAI_USERNAME"] -password = env["NAI_PASSWORD"] -PROXY = env["NAI_PROXY"] if "NAI_PROXY" in env else None - - -async def keystore_integrity(api: NovelAIAPI): - api.timeout = 30 - api.proxy = PROXY +@error_handler(wait=0) +async def keystore_integrity(handle: API): + """ + Verify the integrity of the keystore on decryption - encryption + """ - await api.high_level.login(username, password) + api = handle.api + key = handle.encryption_key - key = get_encryption_key(username, password) keystore = await api.high_level.get_keystore(key) encrypted_keystore_in = [str(keystore.data)] @@ -63,30 +53,23 @@ async def keystore_integrity(api: NovelAIAPI): assert compare_in_out("keystore", encrypted_keystore_in, encrypted_keystore_out) -async def test_keystore_integrity_sync(): - # sync handler - api = NovelAIAPI() - await keystore_integrity(api) +async def test_keystore_integrity_sync(api_handle_sync): # noqa: F811 # pylint: disable=W0621 + await keystore_integrity(api_handle_sync) -async def test_keystore_integrity_async(): - # async handler - try: - async with ClientSession() as session: - api = NovelAIAPI(session) - await keystore_integrity(api) - except Exception as e: - await session.close() - raise e +async def test_keystore_integrity_async(api_handle): # noqa: F811 # pylint: disable=W0621 + await keystore_integrity(api_handle) -async def stories_integrity(api: NovelAIAPI): - api.timeout = 30 - api.proxy = PROXY +@error_handler(wait=0) +async def stories_integrity(handle: API): + """ + Verify the integrity of 'stories' objects on decryption - encryption + """ - await api.high_level.login(username, password) + api = handle.api + key = handle.encryption_key - key = get_encryption_key(username, password) keystore = await api.high_level.get_keystore(key) stories = await api.high_level.download_user_stories() @@ -98,30 +81,23 @@ async def stories_integrity(api: NovelAIAPI): assert compare_in_out("stories", encrypted_stories_in, encrypted_stories_out) -async def test_stories_integrity_sync(): - # sync handler - api = NovelAIAPI() - await stories_integrity(api) +async def test_stories_integrity_sync(api_handle_sync): # noqa: F811 # pylint: disable=W0621 + await stories_integrity(api_handle_sync) -async def test_stories_integrity_async(): - # async handler - try: - async with ClientSession() as session: - api = NovelAIAPI(session) - await stories_integrity(api) - except Exception as e: - await session.close() - raise e +async def test_stories_integrity_async(api_handle): # noqa: F811 # pylint: disable=W0621 + await stories_integrity(api_handle) -async def storycontent_integrity(api: NovelAIAPI): - api.timeout = 30 - api.proxy = PROXY +@error_handler(wait=0) +async def storycontent_integrity(handle: API): + """ + Verify the integrity of 'storycontent' objects on decryption - encryption + """ - await api.high_level.login(username, password) + api = handle.api + key = handle.encryption_key - key = get_encryption_key(username, password) keystore = await api.high_level.get_keystore(key) story_contents = await api.high_level.download_user_story_contents() @@ -139,28 +115,21 @@ async def storycontent_integrity(api: NovelAIAPI): assert compare_in_out("storycontent", decrypted_storycontent_in, decrypted_storycontent_out) -async def test_storycontent_integrity_sync(): - # sync handler - api = NovelAIAPI() - await storycontent_integrity(api) +async def test_storycontent_integrity_sync(api_handle_sync): # noqa: F811 # pylint: disable=W0621 + await storycontent_integrity(api_handle_sync) -async def test_storycontent_integrity_async(): - # async handler - try: - async with ClientSession() as session: - api = NovelAIAPI(session) - await storycontent_integrity(api) - except Exception as e: - await session.close() - raise e +async def test_storycontent_integrity_async(api_handle): # noqa: F811 # pylint: disable=W0621 + await storycontent_integrity(api_handle) -async def presets_integrity(api: NovelAIAPI): - api.timeout = 30 - api.proxy = PROXY +@error_handler(wait=0) +async def presets_integrity(handle: API): + """ + Verify the integrity of 'presets' objects on decompression - compression + """ - await api.high_level.login(username, password) + api = handle.api presets = await api.high_level.download_user_presets() encrypted_presets_in = [str(preset) for preset in presets] @@ -171,30 +140,23 @@ async def presets_integrity(api: NovelAIAPI): assert compare_in_out("presets", encrypted_presets_in, encrypted_presets_out) -async def test_presets_integrity_sync(): - # sync handler - api = NovelAIAPI() - await presets_integrity(api) +async def test_presets_integrity_sync(api_handle_sync): # noqa: F811 # pylint: disable=W0621 + await presets_integrity(api_handle_sync) -async def test_presets_integrity_async(): - # async handler - try: - async with ClientSession() as session: - api = NovelAIAPI(session) - await presets_integrity(api) - except Exception as e: - await session.close() - raise e +async def test_presets_integrity_async(api_handle): # noqa: F811 # pylint: disable=W0621 + await presets_integrity(api_handle) -async def aimodules_integrity(api: NovelAIAPI): - api.timeout = 30 - api.proxy = PROXY +@error_handler(wait=0) +async def aimodules_integrity(handle: API): + """ + Verify the integrity of 'aimodules' objects on decryption - encryption + """ - await api.high_level.login(username, password) + api = handle.api + key = handle.encryption_key - key = get_encryption_key(username, password) keystore = await api.high_level.get_keystore(key) modules = await api.high_level.download_user_modules() @@ -206,28 +168,21 @@ async def aimodules_integrity(api: NovelAIAPI): assert compare_in_out("aimodules", encrypted_modules_in, encrypted_modules_out) -async def test_aimodules_integrity_sync(): - # sync handler - api = NovelAIAPI() - await aimodules_integrity(api) +async def test_aimodules_integrity_sync(api_handle_sync): # noqa: F811 # pylint: disable=W0621 + await aimodules_integrity(api_handle_sync) -async def test_aimodules_integrity_async(): - # async handler - try: - async with ClientSession() as session: - api = NovelAIAPI(session) - await aimodules_integrity(api) - except Exception as e: - await session.close() - raise e +async def test_aimodules_integrity_async(api_handle): # noqa: F811 # pylint: disable=W0621 + await aimodules_integrity(api_handle) -async def shelves_integrity(api: NovelAIAPI): - api.timeout = 30 - api.proxy = PROXY +@error_handler(wait=0) +async def shelves_integrity(handle: API): + """ + Verify the integrity of 'shelves' objects on decompression - compression + """ - await api.high_level.login(username, password) + api = handle.api shelves = await api.high_level.download_user_shelves() encrypted_shelves_in = [str(shelf) for shelf in shelves] @@ -238,39 +193,29 @@ async def shelves_integrity(api: NovelAIAPI): assert compare_in_out("shelves", encrypted_shelves_in, encrypted_shelves_out) -async def test_shelves_integrity_sync(): - # sync handler - api = NovelAIAPI() - await shelves_integrity(api) +async def test_shelves_integrity_sync(api_handle_sync): # noqa: F811 # pylint: disable=W0621 + await shelves_integrity(api_handle_sync) -async def test_shelves_integrity_async(): - # async handler - try: - async with ClientSession() as session: - api = NovelAIAPI(session) - await shelves_integrity(api) - except Exception as e: - await session.close() - raise e +async def test_shelves_integrity_async(api_handle): # noqa: F811 # pylint: disable=W0621 + await shelves_integrity(api_handle) if __name__ == "__main__": async def main(): - await test_keystore_integrity_sync() - await test_keystore_integrity_async() - - await test_stories_integrity_sync() - await test_stories_integrity_async() - - await test_storycontent_integrity_sync() - await test_storycontent_integrity_async() - - await test_presets_integrity_sync() - await test_presets_integrity_async() - - await test_shelves_integrity_sync() - await test_shelves_integrity_async() + async with API() as api: + await test_keystore_integrity_async(api) + await test_stories_integrity_async(api) + await test_storycontent_integrity_async(api) + await test_presets_integrity_async(api) + await test_shelves_integrity_async(api) + + async with API(sync=True) as api: + await test_keystore_integrity_sync(api) + await test_stories_integrity_sync(api) + await test_storycontent_integrity_sync(api) + await test_presets_integrity_sync(api) + await test_shelves_integrity_sync(api) run(main()) diff --git a/tests/api/test_imagegen_samplers.py b/tests/api/test_imagegen_samplers.py index a5300d4..fb249ee 100644 --- a/tests/api/test_imagegen_samplers.py +++ b/tests/api/test_imagegen_samplers.py @@ -1,7 +1,4 @@ """ -{filename} -============================================================================== - Test which samplers currently work """ @@ -34,10 +31,6 @@ async def test_samplers( api_handle, model_sampler: Tuple[ImageModel, ImagePreset] # noqa: F811 # pylint: disable=W0621 ): - """ - Test the presets to ensure they work with the API - """ - api = api_handle.api model, sampler = model_sampler diff --git a/tests/api/test_sync_gen.py b/tests/api/test_sync_gen.py index 65eb662..738ba08 100644 --- a/tests/api/test_sync_gen.py +++ b/tests/api/test_sync_gen.py @@ -1,7 +1,4 @@ """ -{filename} -============================================================================== - | Test if sync capabilities work without problem | This test only checks if sync works, not if the result is right, it's the job of the other tests """ diff --git a/tests/api/test_textgen_presets.py b/tests/api/test_textgen_presets.py index ce26100..84ed172 100644 --- a/tests/api/test_textgen_presets.py +++ b/tests/api/test_textgen_presets.py @@ -1,7 +1,4 @@ """ -{filename} -============================================================================== - Tests pertaining to the Preset class """ diff --git a/tests/api/test_textgen_sanity.py b/tests/api/test_textgen_sanity.py index 789e154..30485a3 100644 --- a/tests/api/test_textgen_sanity.py +++ b/tests/api/test_textgen_sanity.py @@ -1,7 +1,4 @@ """ -{filename} -============================================================================== - Test if the generated content is consistent with the frontend """ @@ -31,7 +28,7 @@ @pytest.mark.parametrize("model_config", model_configs) @error_handler -async def test_generate(api_handle, model_config: Tuple[Model, Path]): # noqa: F811 # pylint: disable=W0621 +async def test_textgen_sanity(api_handle, model_config: Tuple[Model, Path]): # noqa: F811 # pylint: disable=W0621 api = api_handle.api logger = api.logger