From 10c950b3bdf30e63145bb9ff060107aa0e0e2276 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 May 2019 17:10:00 -0700 Subject: [PATCH 01/17] Simplify async, taking advantadge of Python3.4 deprecation --- CHANGELOG.md | 3 ++ hug/__init__.py | 1 - hug/_async.py | 57 ----------------------------------- hug/api.py | 6 ++-- hug/decorators.py | 3 +- hug/development_runner.py | 3 +- hug/input_format.py | 1 - hug/interface.py | 15 +++++++-- hug/output_format.py | 1 - hug/route.py | 3 +- hug/routing.py | 3 +- hug/use.py | 3 +- tests/fixtures.py | 3 +- tests/test_api.py | 3 +- tests/test_authentication.py | 3 +- tests/test_context_factory.py | 3 +- tests/test_decorators.py | 9 +++--- tests/test_directives.py | 3 +- tests/test_documentation.py | 3 +- tests/test_exceptions.py | 3 +- tests/test_input_format.py | 3 +- tests/test_interface.py | 3 +- tests/test_middleware.py | 3 +- tests/test_output_format.py | 3 +- tests/test_redirect.py | 3 +- tests/test_store.py | 1 - tests/test_test.py | 3 +- tests/test_types.py | 7 ++--- tests/test_use.py | 3 +- 29 files changed, 45 insertions(+), 113 deletions(-) delete mode 100644 hug/_async.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 02e9b265..245e433f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ Ideally, within a virtual environment. Changelog ========= +### 2.5.1 - TBD +- Optimizations and simplification of async support, taking advantadge of Python3.4 deprecation. + ### 2.5.0 - May 4, 2019 - Updated to latest Falcon: 2.0.0 - Added support for Marshmallow 3 diff --git a/hug/__init__.py b/hug/__init__.py index beb194e9..31db878e 100644 --- a/hug/__init__.py +++ b/hug/__init__.py @@ -32,7 +32,6 @@ from __future__ import absolute_import from falcon import * - from hug import (authentication, directives, exceptions, format, input_format, introspect, middleware, output_format, redirect, route, test, transform, types, use, validate) from hug._version import current diff --git a/hug/_async.py b/hug/_async.py deleted file mode 100644 index c7000ec0..00000000 --- a/hug/_async.py +++ /dev/null @@ -1,57 +0,0 @@ -"""hug/_async.py - -Defines all required async glue code - -Copyright (C) 2016 Timothy Edmund Crosley - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and -to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -""" -import sys - -try: - import asyncio - - if sys.version_info >= (3, 4, 4): - ensure_future = asyncio.ensure_future # pragma: no cover - else: - ensure_future = getattr('asyncio', 'async') # pragma: no cover - - def asyncio_call(function, *args, **kwargs): - try: - loop = asyncio.get_event_loop() - except RuntimeError: # pragma: no cover - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - if loop.is_running(): - return function(*args, **kwargs) - - function = ensure_future(function(*args, **kwargs), loop=loop) - loop.run_until_complete(function) - return function.result() - - coroutine = asyncio.coroutine - -except ImportError: # pragma: no cover - asyncio = None - - def asyncio_call(*args, **kwargs): - raise NotImplementedError() - - def ensure_future(*args, **kwargs): - raise NotImplementedError() - - def coroutine(function): - return function diff --git a/hug/api.py b/hug/api.py index 22367954..b6869bb4 100644 --- a/hug/api.py +++ b/hug/api.py @@ -21,6 +21,7 @@ """ from __future__ import absolute_import +import asyncio import sys from collections import OrderedDict, namedtuple from distutils.util import strtobool @@ -30,14 +31,13 @@ from wsgiref.simple_server import make_server import falcon -from falcon import HTTP_METHODS - import hug.defaults import hug.output_format +from falcon import HTTP_METHODS from hug import introspect -from hug._async import asyncio, ensure_future from hug._version import current + INTRO = """ /#######################################################################\\ `.----``..-------..``.----. diff --git a/hug/decorators.py b/hug/decorators.py index 0f749bbe..8be894f2 100644 --- a/hug/decorators.py +++ b/hug/decorators.py @@ -29,11 +29,10 @@ import functools from collections import namedtuple -from falcon import HTTP_METHODS - import hug.api import hug.defaults import hug.output_format +from falcon import HTTP_METHODS from hug import introspect from hug.format import underscore diff --git a/hug/development_runner.py b/hug/development_runner.py index 58edc2c2..149949d9 100644 --- a/hug/development_runner.py +++ b/hug/development_runner.py @@ -29,12 +29,13 @@ from multiprocessing import Process from os.path import exists -import _thread as thread from hug._version import current from hug.api import API from hug.route import cli from hug.types import boolean, number +import _thread as thread + INIT_MODULES = list(sys.modules.keys()) diff --git a/hug/input_format.py b/hug/input_format.py index c8671744..cbbdcf17 100644 --- a/hug/input_format.py +++ b/hug/input_format.py @@ -26,7 +26,6 @@ from urllib.parse import parse_qs as urlencoded_converter from falcon.util.uri import parse_query_string - from hug.format import content_type, underscore from hug.json_module import json as json_converter diff --git a/hug/interface.py b/hug/interface.py index 61bb7801..34fc3b60 100644 --- a/hug/interface.py +++ b/hug/interface.py @@ -22,25 +22,34 @@ from __future__ import absolute_import import argparse +import asyncio import os import sys from collections import OrderedDict from functools import lru_cache, partial, wraps import falcon -from falcon import HTTP_BAD_REQUEST - import hug._empty as empty import hug.api import hug.output_format import hug.types as types +from falcon import HTTP_BAD_REQUEST from hug import introspect -from hug._async import asyncio_call from hug.exceptions import InvalidTypeData from hug.format import parse_content_type from hug.types import MarshmallowInputSchema, MarshmallowReturnSchema, Multiple, OneOf, SmartBoolean, Text, text +def asyncio_call(function, *args, **kwargs): + loop = asyncio.get_event_loop() + if loop.is_running(): + return function(*args, **kwargs) + + function = asyncio.ensure_future(function(*args, **kwargs), loop=loop) + loop.run_until_complete(function) + return function.result() + + class Interfaces(object): """Defines the per-function singleton applied to hugged functions defining common data needed by all interfaces""" diff --git a/hug/output_format.py b/hug/output_format.py index a069b597..75be5ba7 100644 --- a/hug/output_format.py +++ b/hug/output_format.py @@ -35,7 +35,6 @@ import falcon from falcon import HTTP_NOT_FOUND - from hug import introspect from hug.format import camelcase, content_type from hug.json_module import json as json_converter diff --git a/hug/route.py b/hug/route.py index 26eb9f10..6f7bc173 100644 --- a/hug/route.py +++ b/hug/route.py @@ -24,9 +24,8 @@ from functools import partial from types import FunctionType, MethodType -from falcon import HTTP_METHODS - import hug.api +from falcon import HTTP_METHODS from hug.routing import CLIRouter as cli from hug.routing import ExceptionRouter as exception from hug.routing import LocalRouter as local diff --git a/hug/routing.py b/hug/routing.py index c05f8ce8..e8fe6ed5 100644 --- a/hug/routing.py +++ b/hug/routing.py @@ -29,11 +29,10 @@ from urllib.parse import urljoin import falcon -from falcon import HTTP_METHODS - import hug.api import hug.interface import hug.output_format +from falcon import HTTP_METHODS from hug import introspect from hug.exceptions import InvalidTypeData diff --git a/hug/use.py b/hug/use.py index b1c8de2e..a72abcb2 100644 --- a/hug/use.py +++ b/hug/use.py @@ -28,9 +28,8 @@ from queue import Queue import falcon -import requests - import hug._empty as empty +import requests from hug.api import API from hug.defaults import input_format from hug.format import parse_content_type diff --git a/tests/fixtures.py b/tests/fixtures.py index b142c69d..0036a135 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -2,9 +2,8 @@ from collections import namedtuple from random import randint -import pytest - import hug +import pytest Routers = namedtuple('Routers', ['http', 'local', 'cli']) diff --git a/tests/test_api.py b/tests/test_api.py index 885a33ef..e926a6cc 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -19,9 +19,8 @@ OTHER DEALINGS IN THE SOFTWARE. """ -import pytest - import hug +import pytest api = hug.API(__name__) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 82ab00a7..1768f768 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -21,9 +21,8 @@ """ from base64 import b64encode -from falcon import HTTPUnauthorized - import hug +from falcon import HTTPUnauthorized api = hug.API(__name__) diff --git a/tests/test_context_factory.py b/tests/test_context_factory.py index ef07b890..a4d11c7b 100644 --- a/tests/test_context_factory.py +++ b/tests/test_context_factory.py @@ -1,11 +1,10 @@ import sys +import hug import pytest from marshmallow import Schema, fields from marshmallow.decorators import post_dump -import hug - module = sys.modules[__name__] diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 7541f09b..821cbba6 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -19,6 +19,7 @@ OTHER DEALINGS IN THE SOFTWARE. """ +import asyncio import json import os import sys @@ -26,15 +27,13 @@ from unittest import mock import falcon +import hug import marshmallow import pytest import requests from falcon.testing import StartResponseMock, create_environ -from marshmallow import ValidationError - -import hug -from hug._async import coroutine from hug.exceptions import InvalidTypeData +from marshmallow import ValidationError from .constants import BASE_DIRECTORY @@ -1476,7 +1475,7 @@ def happens_on_startup(api): pass @hug.startup() - @coroutine + @asyncio.coroutine def async_happens_on_startup(api): pass diff --git a/tests/test_directives.py b/tests/test_directives.py index 40caa288..19d690d4 100644 --- a/tests/test_directives.py +++ b/tests/test_directives.py @@ -21,9 +21,8 @@ """ from base64 import b64encode -import pytest - import hug +import pytest api = hug.API(__name__) diff --git a/tests/test_documentation.py b/tests/test_documentation.py index ed36e433..db5948d2 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -22,12 +22,11 @@ import json from unittest import mock +import hug import marshmallow from falcon import Request from falcon.testing import StartResponseMock, create_environ -import hug - api = hug.API(__name__) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index c8626fd8..bab454a9 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -19,9 +19,8 @@ OTHER DEALINGS IN THE SOFTWARE. """ -import pytest - import hug +import pytest def test_invalid_type_data(): diff --git a/tests/test_input_format.py b/tests/test_input_format.py index afd5cdd1..76d2892b 100644 --- a/tests/test_input_format.py +++ b/tests/test_input_format.py @@ -23,9 +23,8 @@ from cgi import parse_header from io import BytesIO -import requests - import hug +import requests from .constants import BASE_DIRECTORY diff --git a/tests/test_interface.py b/tests/test_interface.py index ea678b76..a823b5c3 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -19,9 +19,8 @@ OTHER DEALINGS IN THE SOFTWARE. """ -import pytest - import hug +import pytest @hug.http(('/namer', '/namer/{name}'), ('GET', 'POST'), versions=(None, 2)) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index aad7c69a..f91d117f 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -19,9 +19,8 @@ """ from http.cookies import SimpleCookie -import pytest - import hug +import pytest from hug.exceptions import SessionNotFound from hug.middleware import CORSMiddleware, LogMiddleware, SessionMiddleware from hug.store import InMemoryStore diff --git a/tests/test_output_format.py b/tests/test_output_format.py index d9fe3455..f158111f 100644 --- a/tests/test_output_format.py +++ b/tests/test_output_format.py @@ -26,11 +26,10 @@ from io import BytesIO from uuid import UUID +import hug import numpy import pytest -import hug - from .constants import BASE_DIRECTORY diff --git a/tests/test_redirect.py b/tests/test_redirect.py index 9f181068..68a8cde8 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -20,9 +20,8 @@ """ import falcon -import pytest - import hug.redirect +import pytest def test_to(): diff --git a/tests/test_store.py b/tests/test_store.py index 2e0eb332..f50d0c2f 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -20,7 +20,6 @@ """ import pytest - from hug.exceptions import StoreKeyNotFound from hug.store import InMemoryStore diff --git a/tests/test_test.py b/tests/test_test.py index 74998646..3bc5b8ec 100644 --- a/tests/test_test.py +++ b/tests/test_test.py @@ -19,9 +19,8 @@ OTHER DEALINGS IN THE SOFTWARE. """ -import pytest - import hug +import pytest api = hug.API(__name__) diff --git a/tests/test_types.py b/tests/test_types.py index 3789a22d..64071946 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -25,12 +25,11 @@ from decimal import Decimal from uuid import UUID -import pytest -from marshmallow import Schema, fields, ValidationError -from marshmallow.decorators import validates_schema - import hug +import pytest from hug.exceptions import InvalidTypeData +from marshmallow import Schema, ValidationError, fields +from marshmallow.decorators import validates_schema api = hug.API(__name__) diff --git a/tests/test_use.py b/tests/test_use.py index c35bd085..a74d0541 100644 --- a/tests/test_use.py +++ b/tests/test_use.py @@ -23,10 +23,9 @@ import socket import struct +import hug import pytest import requests - -import hug from hug import use From d62d6915f4adb2644f4043abc030de057fcf4039 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Sun, 5 May 2019 17:12:13 -0700 Subject: [PATCH 02/17] Update to latest version of isort --- hug/__init__.py | 1 + hug/api.py | 4 ++-- hug/decorators.py | 3 ++- hug/development_runner.py | 3 +-- hug/input_format.py | 1 + hug/interface.py | 3 ++- hug/output_format.py | 1 + hug/route.py | 3 ++- hug/routing.py | 3 ++- hug/test.py | 2 +- hug/use.py | 3 ++- requirements/development.txt | 2 +- tests/fixtures.py | 3 ++- tests/test_api.py | 3 ++- tests/test_authentication.py | 3 ++- tests/test_context_factory.py | 3 ++- tests/test_decorators.py | 5 +++-- tests/test_directives.py | 3 ++- tests/test_documentation.py | 3 ++- tests/test_exceptions.py | 3 ++- tests/test_input_format.py | 3 ++- tests/test_interface.py | 3 ++- tests/test_middleware.py | 3 ++- tests/test_output_format.py | 3 ++- tests/test_redirect.py | 3 ++- tests/test_store.py | 1 + tests/test_test.py | 3 ++- tests/test_types.py | 5 +++-- tests/test_use.py | 3 ++- 29 files changed, 53 insertions(+), 29 deletions(-) diff --git a/hug/__init__.py b/hug/__init__.py index 31db878e..beb194e9 100644 --- a/hug/__init__.py +++ b/hug/__init__.py @@ -32,6 +32,7 @@ from __future__ import absolute_import from falcon import * + from hug import (authentication, directives, exceptions, format, input_format, introspect, middleware, output_format, redirect, route, test, transform, types, use, validate) from hug._version import current diff --git a/hug/api.py b/hug/api.py index b6869bb4..c20c8fe7 100644 --- a/hug/api.py +++ b/hug/api.py @@ -31,13 +31,13 @@ from wsgiref.simple_server import make_server import falcon +from falcon import HTTP_METHODS + import hug.defaults import hug.output_format -from falcon import HTTP_METHODS from hug import introspect from hug._version import current - INTRO = """ /#######################################################################\\ `.----``..-------..``.----. diff --git a/hug/decorators.py b/hug/decorators.py index 8be894f2..0f749bbe 100644 --- a/hug/decorators.py +++ b/hug/decorators.py @@ -29,10 +29,11 @@ import functools from collections import namedtuple +from falcon import HTTP_METHODS + import hug.api import hug.defaults import hug.output_format -from falcon import HTTP_METHODS from hug import introspect from hug.format import underscore diff --git a/hug/development_runner.py b/hug/development_runner.py index 149949d9..58edc2c2 100644 --- a/hug/development_runner.py +++ b/hug/development_runner.py @@ -29,13 +29,12 @@ from multiprocessing import Process from os.path import exists +import _thread as thread from hug._version import current from hug.api import API from hug.route import cli from hug.types import boolean, number -import _thread as thread - INIT_MODULES = list(sys.modules.keys()) diff --git a/hug/input_format.py b/hug/input_format.py index cbbdcf17..c8671744 100644 --- a/hug/input_format.py +++ b/hug/input_format.py @@ -26,6 +26,7 @@ from urllib.parse import parse_qs as urlencoded_converter from falcon.util.uri import parse_query_string + from hug.format import content_type, underscore from hug.json_module import json as json_converter diff --git a/hug/interface.py b/hug/interface.py index 34fc3b60..cc5c5cb0 100644 --- a/hug/interface.py +++ b/hug/interface.py @@ -29,11 +29,12 @@ from functools import lru_cache, partial, wraps import falcon +from falcon import HTTP_BAD_REQUEST + import hug._empty as empty import hug.api import hug.output_format import hug.types as types -from falcon import HTTP_BAD_REQUEST from hug import introspect from hug.exceptions import InvalidTypeData from hug.format import parse_content_type diff --git a/hug/output_format.py b/hug/output_format.py index 75be5ba7..a069b597 100644 --- a/hug/output_format.py +++ b/hug/output_format.py @@ -35,6 +35,7 @@ import falcon from falcon import HTTP_NOT_FOUND + from hug import introspect from hug.format import camelcase, content_type from hug.json_module import json as json_converter diff --git a/hug/route.py b/hug/route.py index 6f7bc173..26eb9f10 100644 --- a/hug/route.py +++ b/hug/route.py @@ -24,8 +24,9 @@ from functools import partial from types import FunctionType, MethodType -import hug.api from falcon import HTTP_METHODS + +import hug.api from hug.routing import CLIRouter as cli from hug.routing import ExceptionRouter as exception from hug.routing import LocalRouter as local diff --git a/hug/routing.py b/hug/routing.py index e8fe6ed5..c05f8ce8 100644 --- a/hug/routing.py +++ b/hug/routing.py @@ -29,10 +29,11 @@ from urllib.parse import urljoin import falcon +from falcon import HTTP_METHODS + import hug.api import hug.interface import hug.output_format -from falcon import HTTP_METHODS from hug import introspect from hug.exceptions import InvalidTypeData diff --git a/hug/test.py b/hug/test.py index 7e48aacd..5e7c91f3 100644 --- a/hug/test.py +++ b/hug/test.py @@ -29,7 +29,7 @@ from urllib.parse import urlencode from falcon import HTTP_METHODS -from falcon.testing import StartResponseMock, create_environ, DEFAULT_HOST +from falcon.testing import DEFAULT_HOST, StartResponseMock, create_environ from hug import output_format from hug.api import API diff --git a/hug/use.py b/hug/use.py index a72abcb2..b1c8de2e 100644 --- a/hug/use.py +++ b/hug/use.py @@ -28,8 +28,9 @@ from queue import Queue import falcon -import hug._empty as empty import requests + +import hug._empty as empty from hug.api import API from hug.defaults import input_format from hug.format import parse_content_type diff --git a/requirements/development.txt b/requirements/development.txt index 6e344e1f..d967da5a 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -3,7 +3,7 @@ Cython==0.29.6 -r common.txt flake8==3.5.0 ipython==6.2.1 -isort==4.2.5 +isort==4.3.18 pytest-cov==2.5.1 pytest==4.3.1 python-coveralls==2.9.1 diff --git a/tests/fixtures.py b/tests/fixtures.py index 0036a135..b142c69d 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -2,9 +2,10 @@ from collections import namedtuple from random import randint -import hug import pytest +import hug + Routers = namedtuple('Routers', ['http', 'local', 'cli']) diff --git a/tests/test_api.py b/tests/test_api.py index e926a6cc..885a33ef 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -19,9 +19,10 @@ OTHER DEALINGS IN THE SOFTWARE. """ -import hug import pytest +import hug + api = hug.API(__name__) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 1768f768..82ab00a7 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -21,9 +21,10 @@ """ from base64 import b64encode -import hug from falcon import HTTPUnauthorized +import hug + api = hug.API(__name__) diff --git a/tests/test_context_factory.py b/tests/test_context_factory.py index a4d11c7b..ef07b890 100644 --- a/tests/test_context_factory.py +++ b/tests/test_context_factory.py @@ -1,10 +1,11 @@ import sys -import hug import pytest from marshmallow import Schema, fields from marshmallow.decorators import post_dump +import hug + module = sys.modules[__name__] diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 821cbba6..5c593fe6 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -27,14 +27,15 @@ from unittest import mock import falcon -import hug import marshmallow import pytest import requests from falcon.testing import StartResponseMock, create_environ -from hug.exceptions import InvalidTypeData from marshmallow import ValidationError +import hug +from hug.exceptions import InvalidTypeData + from .constants import BASE_DIRECTORY api = hug.API(__name__) diff --git a/tests/test_directives.py b/tests/test_directives.py index 19d690d4..40caa288 100644 --- a/tests/test_directives.py +++ b/tests/test_directives.py @@ -21,9 +21,10 @@ """ from base64 import b64encode -import hug import pytest +import hug + api = hug.API(__name__) # Fix flake8 undefined names (F821) diff --git a/tests/test_documentation.py b/tests/test_documentation.py index db5948d2..ed36e433 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -22,11 +22,12 @@ import json from unittest import mock -import hug import marshmallow from falcon import Request from falcon.testing import StartResponseMock, create_environ +import hug + api = hug.API(__name__) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index bab454a9..c8626fd8 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -19,9 +19,10 @@ OTHER DEALINGS IN THE SOFTWARE. """ -import hug import pytest +import hug + def test_invalid_type_data(): try: diff --git a/tests/test_input_format.py b/tests/test_input_format.py index 76d2892b..afd5cdd1 100644 --- a/tests/test_input_format.py +++ b/tests/test_input_format.py @@ -23,9 +23,10 @@ from cgi import parse_header from io import BytesIO -import hug import requests +import hug + from .constants import BASE_DIRECTORY diff --git a/tests/test_interface.py b/tests/test_interface.py index a823b5c3..ea678b76 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -19,9 +19,10 @@ OTHER DEALINGS IN THE SOFTWARE. """ -import hug import pytest +import hug + @hug.http(('/namer', '/namer/{name}'), ('GET', 'POST'), versions=(None, 2)) def namer(name=None): diff --git a/tests/test_middleware.py b/tests/test_middleware.py index f91d117f..aad7c69a 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -19,8 +19,9 @@ """ from http.cookies import SimpleCookie -import hug import pytest + +import hug from hug.exceptions import SessionNotFound from hug.middleware import CORSMiddleware, LogMiddleware, SessionMiddleware from hug.store import InMemoryStore diff --git a/tests/test_output_format.py b/tests/test_output_format.py index f158111f..d9fe3455 100644 --- a/tests/test_output_format.py +++ b/tests/test_output_format.py @@ -26,10 +26,11 @@ from io import BytesIO from uuid import UUID -import hug import numpy import pytest +import hug + from .constants import BASE_DIRECTORY diff --git a/tests/test_redirect.py b/tests/test_redirect.py index 68a8cde8..9f181068 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -20,9 +20,10 @@ """ import falcon -import hug.redirect import pytest +import hug.redirect + def test_to(): """Test that the base redirect to function works as expected""" diff --git a/tests/test_store.py b/tests/test_store.py index f50d0c2f..2e0eb332 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -20,6 +20,7 @@ """ import pytest + from hug.exceptions import StoreKeyNotFound from hug.store import InMemoryStore diff --git a/tests/test_test.py b/tests/test_test.py index 3bc5b8ec..74998646 100644 --- a/tests/test_test.py +++ b/tests/test_test.py @@ -19,9 +19,10 @@ OTHER DEALINGS IN THE SOFTWARE. """ -import hug import pytest +import hug + api = hug.API(__name__) diff --git a/tests/test_types.py b/tests/test_types.py index 64071946..4c3df1e7 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -25,12 +25,13 @@ from decimal import Decimal from uuid import UUID -import hug import pytest -from hug.exceptions import InvalidTypeData from marshmallow import Schema, ValidationError, fields from marshmallow.decorators import validates_schema +import hug +from hug.exceptions import InvalidTypeData + api = hug.API(__name__) diff --git a/tests/test_use.py b/tests/test_use.py index a74d0541..c35bd085 100644 --- a/tests/test_use.py +++ b/tests/test_use.py @@ -23,9 +23,10 @@ import socket import struct -import hug import pytest import requests + +import hug from hug import use From 3d393baed808104b2c3772e4342e7cce8bff49a4 Mon Sep 17 00:00:00 2001 From: Jason Tyler Date: Mon, 6 May 2019 18:45:32 -0700 Subject: [PATCH 03/17] black style reformat; 100 line length --- benchmarks/http/bobo_test.py | 4 +- benchmarks/http/bottle_test.py | 4 +- benchmarks/http/cherrypy_test.py | 3 +- benchmarks/http/falcon_test.py | 7 +- benchmarks/http/flask_test.py | 4 +- benchmarks/http/hug_test.py | 4 +- benchmarks/http/muffin_test.py | 6 +- benchmarks/http/pyramid_test.py | 6 +- benchmarks/http/tornado_test.py | 6 +- benchmarks/internal/argument_populating.py | 13 +- docker/template/__init__.py | 2 +- docker/template/handlers/hello.py | 2 +- examples/authentication.py | 42 +- examples/cli.py | 4 +- examples/cli_object.py | 14 +- examples/cors_middleware.py | 4 +- examples/cors_per_route.py | 2 +- examples/docker_compose_with_mongodb/app.py | 17 +- examples/docker_nginx/api/__main__.py | 2 +- examples/docker_nginx/setup.py | 37 +- examples/file_upload_example.py | 7 +- examples/force_https.py | 2 +- examples/happy_birthday.py | 5 +- examples/hello_world.py | 2 +- examples/html_serve.py | 4 +- examples/image_serve.py | 4 +- examples/marshmallow_example.py | 14 +- examples/multi_file_cli/api.py | 6 +- examples/multi_file_cli/sub_api.py | 2 +- examples/multiple_files/api.py | 2 +- examples/multiple_files/part_1.py | 2 +- examples/multiple_files/part_2.py | 2 +- examples/override_404.py | 4 +- examples/pil_example/pill.py | 6 +- examples/quick_server.py | 4 +- examples/quick_start/first_step_1.py | 3 +- examples/quick_start/first_step_2.py | 5 +- examples/quick_start/first_step_3.py | 7 +- examples/return_400.py | 1 + examples/secure_auth_with_db_example.py | 54 +- examples/sink_example.py | 4 +- examples/smtp_envelope_example.py | 13 +- examples/sqlalchemy_example/demo/api.py | 36 +- examples/sqlalchemy_example/demo/app.py | 2 +- .../sqlalchemy_example/demo/authentication.py | 7 +- examples/sqlalchemy_example/demo/context.py | 1 - .../sqlalchemy_example/demo/directives.py | 3 +- examples/sqlalchemy_example/demo/models.py | 8 +- .../sqlalchemy_example/demo/validation.py | 15 +- examples/static_serve.py | 22 +- .../streaming_movie_server/movie_server.py | 2 +- examples/test_happy_birthday.py | 8 +- examples/unicode_output.py | 2 +- examples/use_socket.py | 10 +- examples/versioning.py | 14 +- examples/write_once.py | 8 +- hug/__init__.py | 65 +- hug/api.py | 235 ++-- hug/authentication.py | 58 +- hug/decorators.py | 39 +- hug/defaults.py | 28 +- hug/development_runner.py | 43 +- hug/directives.py | 20 +- hug/exceptions.py | 2 + hug/format.py | 10 +- hug/input_format.py | 26 +- hug/interface.py | 547 +++++---- hug/introspect.py | 9 +- hug/json_module.py | 7 +- hug/middleware.py | 103 +- hug/output_format.py | 219 ++-- hug/redirect.py | 2 +- hug/route.py | 89 +- hug/routing.py | 315 +++-- hug/store.py | 1 + hug/test.py | 60 +- hug/transform.py | 24 +- hug/types.py | 203 +++- hug/use.py | 149 ++- hug/validate.py | 7 +- setup.py | 85 +- tests/constants.py | 2 +- tests/fixtures.py | 14 +- tests/module_fake.py | 8 +- tests/module_fake_http_and_cli.py | 2 +- tests/module_fake_many_methods.py | 4 +- tests/module_fake_post.py | 2 +- tests/module_fake_simple.py | 8 +- tests/test_api.py | 39 +- tests/test_async.py | 16 +- tests/test_authentication.py | 233 ++-- tests/test_context_factory.py | 288 +++-- tests/test_coroutines.py | 16 +- tests/test_decorators.py | 1066 ++++++++++------- tests/test_directives.py | 73 +- tests/test_documentation.py | 128 +- tests/test_exceptions.py | 8 +- tests/test_global_context.py | 27 +- tests/test_input_format.py | 24 +- tests/test_interface.py | 21 +- tests/test_introspect.py | 67 +- tests/test_middleware.py | 128 +- tests/test_output_format.py | 346 +++--- tests/test_redirect.py | 22 +- tests/test_route.py | 113 +- tests/test_routing.py | 304 +++-- tests/test_store.py | 15 +- tests/test_test.py | 9 +- tests/test_transform.py | 47 +- tests/test_types.py | 627 +++++----- tests/test_use.py | 103 +- tests/test_validate.py | 26 +- 112 files changed, 3810 insertions(+), 2796 deletions(-) diff --git a/benchmarks/http/bobo_test.py b/benchmarks/http/bobo_test.py index eb1f4ed4..843e6ab0 100644 --- a/benchmarks/http/bobo_test.py +++ b/benchmarks/http/bobo_test.py @@ -1,9 +1,9 @@ import bobo -@bobo.query('/text', content_type='text/plain') +@bobo.query("/text", content_type="text/plain") def text(): - return 'Hello, world!' + return "Hello, world!" app = bobo.Application(bobo_resources=__name__) diff --git a/benchmarks/http/bottle_test.py b/benchmarks/http/bottle_test.py index 8a8d7314..49159922 100644 --- a/benchmarks/http/bottle_test.py +++ b/benchmarks/http/bottle_test.py @@ -3,6 +3,6 @@ app = bottle.Bottle() -@app.route('/text') +@app.route("/text") def text(): - return 'Hello, world!' + return "Hello, world!" diff --git a/benchmarks/http/cherrypy_test.py b/benchmarks/http/cherrypy_test.py index e100b679..5dd8e6b0 100644 --- a/benchmarks/http/cherrypy_test.py +++ b/benchmarks/http/cherrypy_test.py @@ -2,10 +2,9 @@ class Root(object): - @cherrypy.expose def text(self): - return 'Hello, world!' + return "Hello, world!" app = cherrypy.tree.mount(Root()) diff --git a/benchmarks/http/falcon_test.py b/benchmarks/http/falcon_test.py index 6f709d61..18891612 100644 --- a/benchmarks/http/falcon_test.py +++ b/benchmarks/http/falcon_test.py @@ -2,12 +2,11 @@ class Resource(object): - def on_get(self, req, resp): resp.status = falcon.HTTP_200 - resp.content_type = 'text/plain' - resp.body = 'Hello, world!' + resp.content_type = "text/plain" + resp.body = "Hello, world!" app = falcon.API() -app.add_route('/text', Resource()) +app.add_route("/text", Resource()) diff --git a/benchmarks/http/flask_test.py b/benchmarks/http/flask_test.py index e585dd31..4da0554a 100644 --- a/benchmarks/http/flask_test.py +++ b/benchmarks/http/flask_test.py @@ -3,6 +3,6 @@ app = flask.Flask(__name__) -@app.route('/text') +@app.route("/text") def text(): - return 'Hello, world!' + return "Hello, world!" diff --git a/benchmarks/http/hug_test.py b/benchmarks/http/hug_test.py index 929d3166..7ac32e54 100644 --- a/benchmarks/http/hug_test.py +++ b/benchmarks/http/hug_test.py @@ -1,9 +1,9 @@ import hug -@hug.get('/text', output_format=hug.output_format.text, parse_body=False) +@hug.get("/text", output_format=hug.output_format.text, parse_body=False) def text(): - return 'Hello, World!' + return "Hello, World!" app = hug.API(__name__).http.server() diff --git a/benchmarks/http/muffin_test.py b/benchmarks/http/muffin_test.py index cf9c9985..88710fcb 100644 --- a/benchmarks/http/muffin_test.py +++ b/benchmarks/http/muffin_test.py @@ -1,8 +1,8 @@ import muffin -app = muffin.Application('web') +app = muffin.Application("web") -@app.register('/text') +@app.register("/text") def text(request): - return 'Hello, World!' + return "Hello, World!" diff --git a/benchmarks/http/pyramid_test.py b/benchmarks/http/pyramid_test.py index b6404f26..76b4364f 100644 --- a/benchmarks/http/pyramid_test.py +++ b/benchmarks/http/pyramid_test.py @@ -2,14 +2,14 @@ from pyramid.config import Configurator -@view_config(route_name='text', renderer='string') +@view_config(route_name="text", renderer="string") def text(request): - return 'Hello, World!' + return "Hello, World!" config = Configurator() -config.add_route('text', '/text') +config.add_route("text", "/text") config.scan() app = config.make_wsgi_app() diff --git a/benchmarks/http/tornado_test.py b/benchmarks/http/tornado_test.py index c703bb41..a8e06097 100755 --- a/benchmarks/http/tornado_test.py +++ b/benchmarks/http/tornado_test.py @@ -6,12 +6,10 @@ class TextHandler(tornado.web.RequestHandler): def get(self): - self.write('Hello, world!') + self.write("Hello, world!") -application = tornado.web.Application([ - (r"/text", TextHandler), -]) +application = tornado.web.Application([(r"/text", TextHandler)]) if __name__ == "__main__": application.listen(8000) diff --git a/benchmarks/internal/argument_populating.py b/benchmarks/internal/argument_populating.py index da38303e..c5becfa7 100644 --- a/benchmarks/internal/argument_populating.py +++ b/benchmarks/internal/argument_populating.py @@ -3,11 +3,10 @@ from hug.decorators import auto_kwargs from hug.introspect import generate_accepted_kwargs -DATA = {'request': None} +DATA = {"request": None} class Timer(object): - def __init__(self, name): self.name = name @@ -26,25 +25,25 @@ def my_method_with_kwargs(name, request=None, **kwargs): pass -with Timer('generate_kwargs'): - accept_kwargs = generate_accepted_kwargs(my_method, ('request', 'response', 'version')) +with Timer("generate_kwargs"): + accept_kwargs = generate_accepted_kwargs(my_method, ("request", "response", "version")) for test in range(100000): my_method(test, **accept_kwargs(DATA)) -with Timer('auto_kwargs'): +with Timer("auto_kwargs"): wrapped_method = auto_kwargs(my_method) for test in range(100000): wrapped_method(test, **DATA) -with Timer('native_kwargs'): +with Timer("native_kwargs"): for test in range(100000): my_method_with_kwargs(test, **DATA) -with Timer('no_kwargs'): +with Timer("no_kwargs"): for test in range(100000): my_method(test, request=None) diff --git a/docker/template/__init__.py b/docker/template/__init__.py index 2ec76b39..3d060614 100644 --- a/docker/template/__init__.py +++ b/docker/template/__init__.py @@ -2,6 +2,6 @@ from handlers import birthday, hello -@hug.extend_api('') +@hug.extend_api("") def api(): return [hello, birthday] diff --git a/docker/template/handlers/hello.py b/docker/template/handlers/hello.py index e6b2f4eb..be28cdf3 100644 --- a/docker/template/handlers/hello.py +++ b/docker/template/handlers/hello.py @@ -2,5 +2,5 @@ @hug.get("/hello") -def hello(name: str="World"): +def hello(name: str = "World"): return "Hello, {name}".format(name=name) diff --git a/examples/authentication.py b/examples/authentication.py index 0796f3e5..26d7d4d7 100644 --- a/examples/authentication.py +++ b/examples/authentication.py @@ -1,4 +1,4 @@ -'''A basic example of authentication requests within a hug API''' +"""A basic example of authentication requests within a hug API""" import hug import jwt @@ -9,10 +9,10 @@ # on successful authentication. Naturally, this is a trivial demo, and a much # more robust verification function is recommended. This is for strictly # illustrative purposes. -authentication = hug.authentication.basic(hug.authentication.verify('User1', 'mypassword')) +authentication = hug.authentication.basic(hug.authentication.verify("User1", "mypassword")) -@hug.get('/public') +@hug.get("/public") def public_api_call(): return "Needs no authentication" @@ -21,9 +21,9 @@ def public_api_call(): # Directives can provide computed input parameters via an abstraction # layer so as not to clutter your API functions with access to the raw # request object. -@hug.get('/authenticated', requires=authentication) +@hug.get("/authenticated", requires=authentication) def basic_auth_api_call(user: hug.directives.user): - return 'Successfully authenticated with user: {0}'.format(user) + return "Successfully authenticated with user: {0}".format(user) # Here is a slightly less trivial example of how authentication might @@ -40,10 +40,10 @@ def __init__(self, user_id, api_key): def api_key_verify(api_key): - magic_key = '5F00832B-DE24-4CAF-9638-C10D1C642C6C' # Obviously, this would hit your database + magic_key = "5F00832B-DE24-4CAF-9638-C10D1C642C6C" # Obviously, this would hit your database if api_key == magic_key: # Success! - return APIUser('user_foo', api_key) + return APIUser("user_foo", api_key) else: # Invalid key return None @@ -52,15 +52,15 @@ def api_key_verify(api_key): api_key_authentication = hug.authentication.api_key(api_key_verify) -@hug.get('/key_authenticated', requires=api_key_authentication) # noqa +@hug.get("/key_authenticated", requires=api_key_authentication) # noqa def basic_auth_api_call(user: hug.directives.user): - return 'Successfully authenticated with user: {0}'.format(user.user_id) + return "Successfully authenticated with user: {0}".format(user.user_id) def token_verify(token): - secret_key = 'super-secret-key-please-change' + secret_key = "super-secret-key-please-change" try: - return jwt.decode(token, secret_key, algorithm='HS256') + return jwt.decode(token, secret_key, algorithm="HS256") except jwt.DecodeError: return False @@ -68,17 +68,19 @@ def token_verify(token): token_key_authentication = hug.authentication.token(token_verify) -@hug.get('/token_authenticated', requires=token_key_authentication) # noqa +@hug.get("/token_authenticated", requires=token_key_authentication) # noqa def token_auth_call(user: hug.directives.user): - return 'You are user: {0} with data {1}'.format(user['user'], user['data']) + return "You are user: {0} with data {1}".format(user["user"], user["data"]) -@hug.post('/token_generation') # noqa +@hug.post("/token_generation") # noqa def token_gen_call(username, password): """Authenticate and return a token""" - secret_key = 'super-secret-key-please-change' - mockusername = 'User2' - mockpassword = 'Mypassword' - if mockpassword == password and mockusername == username: # This is an example. Don't do that. - return {"token" : jwt.encode({'user': username, 'data': 'mydata'}, secret_key, algorithm='HS256')} - return 'Invalid username and/or password for user: {0}'.format(username) + secret_key = "super-secret-key-please-change" + mockusername = "User2" + mockpassword = "Mypassword" + if mockpassword == password and mockusername == username: # This is an example. Don't do that. + return { + "token": jwt.encode({"user": username, "data": "mydata"}, secret_key, algorithm="HS256") + } + return "Invalid username and/or password for user: {0}".format(username) diff --git a/examples/cli.py b/examples/cli.py index 1d7a1d09..efb6a094 100644 --- a/examples/cli.py +++ b/examples/cli.py @@ -3,10 +3,10 @@ @hug.cli(version="1.0.0") -def cli(name: 'The name', age: hug.types.number): +def cli(name: "The name", age: hug.types.number): """Says happy birthday to a user""" return "Happy {age} Birthday {name}!\n".format(**locals()) -if __name__ == '__main__': +if __name__ == "__main__": cli.interface.cli() diff --git a/examples/cli_object.py b/examples/cli_object.py index 74503519..00026539 100644 --- a/examples/cli_object.py +++ b/examples/cli_object.py @@ -1,20 +1,20 @@ import hug -API = hug.API('git') +API = hug.API("git") -@hug.object(name='git', version='1.0.0', api=API) +@hug.object(name="git", version="1.0.0", api=API) class GIT(object): """An example of command like calls via an Object""" @hug.object.cli - def push(self, branch='master'): - return 'Pushing {}'.format(branch) + def push(self, branch="master"): + return "Pushing {}".format(branch) @hug.object.cli - def pull(self, branch='master'): - return 'Pulling {}'.format(branch) + def pull(self, branch="master"): + return "Pulling {}".format(branch) -if __name__ == '__main__': +if __name__ == "__main__": API.cli() diff --git a/examples/cors_middleware.py b/examples/cors_middleware.py index 77156d18..9600534a 100644 --- a/examples/cors_middleware.py +++ b/examples/cors_middleware.py @@ -4,6 +4,6 @@ api.http.add_middleware(hug.middleware.CORSMiddleware(api, max_age=10)) -@hug.get('/demo') +@hug.get("/demo") def get_demo(): - return {'result': 'Hello World'} + return {"result": "Hello World"} diff --git a/examples/cors_per_route.py b/examples/cors_per_route.py index 7d2400b1..75c553f9 100644 --- a/examples/cors_per_route.py +++ b/examples/cors_per_route.py @@ -2,5 +2,5 @@ @hug.get() -def cors_supported(cors: hug.directives.cors="*"): +def cors_supported(cors: hug.directives.cors = "*"): return "Hello world!" diff --git a/examples/docker_compose_with_mongodb/app.py b/examples/docker_compose_with_mongodb/app.py index e4452e35..88f2466e 100644 --- a/examples/docker_compose_with_mongodb/app.py +++ b/examples/docker_compose_with_mongodb/app.py @@ -2,29 +2,28 @@ import hug -client = MongoClient('db', 27017) -db = client['our-database'] -collection = db['our-items'] +client = MongoClient("db", 27017) +db = client["our-database"] +collection = db["our-items"] -@hug.get('/', output=hug.output_format.pretty_json) +@hug.get("/", output=hug.output_format.pretty_json) def show(): """Returns a list of items currently in the database""" items = list(collection.find()) # JSON conversion chokes on the _id objects, so we convert # them to strings here for i in items: - i['_id'] = str(i['_id']) + i["_id"] = str(i["_id"]) return items -@hug.post('/new', status_code=hug.falcon.HTTP_201) +@hug.post("/new", status_code=hug.falcon.HTTP_201) def new(name: hug.types.text, description: hug.types.text): """Inserts the given object as a new item in the database. Returns the ID of the newly created item. """ - item_doc = {'name': name, 'description': description} + item_doc = {"name": name, "description": description} collection.insert_one(item_doc) - return str(item_doc['_id']) - + return str(item_doc["_id"]) diff --git a/examples/docker_nginx/api/__main__.py b/examples/docker_nginx/api/__main__.py index 9f43342d..c16b1689 100644 --- a/examples/docker_nginx/api/__main__.py +++ b/examples/docker_nginx/api/__main__.py @@ -11,4 +11,4 @@ def base(): @hug.get("/add", examples="num=1") def add(num: hug.types.number = 1): - return {"res" : num + 1} + return {"res": num + 1} diff --git a/examples/docker_nginx/setup.py b/examples/docker_nginx/setup.py index 4a3a4c4f..252029ef 100644 --- a/examples/docker_nginx/setup.py +++ b/examples/docker_nginx/setup.py @@ -4,37 +4,26 @@ from setuptools import setup setup( - name = "app-name", - version = "0.0.1", - description = "App Description", - url = "https://github.com/CMoncur/nginx-gunicorn-hug", - author = "Cody Moncur", - author_email = "cmoncur@gmail.com", - classifiers = [ + name="app-name", + version="0.0.1", + description="App Description", + url="https://github.com/CMoncur/nginx-gunicorn-hug", + author="Cody Moncur", + author_email="cmoncur@gmail.com", + classifiers=[ # 3 - Alpha # 4 - Beta # 5 - Production/Stable "Development Status :: 3 - Alpha", - "Programming Language :: Python :: 3.6" + "Programming Language :: Python :: 3.6", ], - packages = [], - + packages=[], # Entry Point - entry_points = { - "console_scripts": [] - }, - + entry_points={"console_scripts": []}, # Core Dependencies - install_requires = [ - "hug" - ], - + install_requires=["hug"], # Dev/Test Dependencies - extras_require = { - "dev": [], - "test": [], - }, - + extras_require={"dev": [], "test": []}, # Scripts - scripts = [] + scripts=[], ) diff --git a/examples/file_upload_example.py b/examples/file_upload_example.py index 69e0019e..10efcece 100644 --- a/examples/file_upload_example.py +++ b/examples/file_upload_example.py @@ -17,9 +17,10 @@ import hug -@hug.post('/upload') + +@hug.post("/upload") def upload_file(body): """accepts file uploads""" # is a simple dictionary of {filename: b'content'} - print('body: ', body) - return {'filename': list(body.keys()).pop(), 'filesize': len(list(body.values()).pop())} + print("body: ", body) + return {"filename": list(body.keys()).pop(), "filesize": len(list(body.values()).pop())} diff --git a/examples/force_https.py b/examples/force_https.py index cbff5d05..ade7ea50 100644 --- a/examples/force_https.py +++ b/examples/force_https.py @@ -10,4 +10,4 @@ @hug.get() def my_endpoint(): - return 'Success!' + return "Success!" diff --git a/examples/happy_birthday.py b/examples/happy_birthday.py index c91a872e..ee80b378 100644 --- a/examples/happy_birthday.py +++ b/examples/happy_birthday.py @@ -2,12 +2,13 @@ import hug -@hug.get('/happy_birthday', examples="name=HUG&age=1") +@hug.get("/happy_birthday", examples="name=HUG&age=1") def happy_birthday(name, age: hug.types.number): """Says happy birthday to a user""" return "Happy {age} Birthday {name}!".format(**locals()) -@hug.get('/greet/{event}') + +@hug.get("/greet/{event}") def greet(event: str): """Greets appropriately (from http://blog.ketchum.com/how-to-write-10-common-holiday-greetings/) """ greetings = "Happy" diff --git a/examples/hello_world.py b/examples/hello_world.py index c7c91c10..6b0329fd 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -4,4 +4,4 @@ @hug.get() def hello(request): """Says hellos""" - return 'Hello Worlds for Bacon?!' + return "Hello Worlds for Bacon?!" diff --git a/examples/html_serve.py b/examples/html_serve.py index 38b54cb7..d8b994e6 100644 --- a/examples/html_serve.py +++ b/examples/html_serve.py @@ -6,10 +6,10 @@ DIRECTORY = os.path.dirname(os.path.realpath(__file__)) -@hug.get('/get/document', output=hug.output_format.html) +@hug.get("/get/document", output=hug.output_format.html) def nagiosCommandHelp(**kwargs): """ Returns command help document when no command is specified """ - with open(os.path.join(DIRECTORY, 'document.html')) as document: + with open(os.path.join(DIRECTORY, "document.html")) as document: return document.read() diff --git a/examples/image_serve.py b/examples/image_serve.py index ba0998d0..27f726dd 100644 --- a/examples/image_serve.py +++ b/examples/image_serve.py @@ -1,7 +1,7 @@ import hug -@hug.get('/image.png', output=hug.output_format.png_image) +@hug.get("/image.png", output=hug.output_format.png_image) def image(): """Serves up a PNG image.""" - return '../artwork/logo.png' + return "../artwork/logo.png" diff --git a/examples/marshmallow_example.py b/examples/marshmallow_example.py index 09fc401c..8bbda223 100644 --- a/examples/marshmallow_example.py +++ b/examples/marshmallow_example.py @@ -22,15 +22,17 @@ from marshmallow.validate import Range, OneOf -@hug.get('/dateadd', examples="value=1973-04-10&addend=63") -def dateadd(value: fields.DateTime(), - addend: fields.Int(validate=Range(min=1)), - unit: fields.Str(validate=OneOf(['minutes', 'days']))='days'): +@hug.get("/dateadd", examples="value=1973-04-10&addend=63") +def dateadd( + value: fields.DateTime(), + addend: fields.Int(validate=Range(min=1)), + unit: fields.Str(validate=OneOf(["minutes", "days"])) = "days", +): """Add a value to a date.""" value = value or dt.datetime.utcnow() - if unit == 'minutes': + if unit == "minutes": delta = dt.timedelta(minutes=addend) else: delta = dt.timedelta(days=addend) result = value + delta - return {'result': result} + return {"result": result} diff --git a/examples/multi_file_cli/api.py b/examples/multi_file_cli/api.py index 179f6385..f7242804 100644 --- a/examples/multi_file_cli/api.py +++ b/examples/multi_file_cli/api.py @@ -8,10 +8,10 @@ def echo(text: hug.types.text): return text -@hug.extend_api(sub_command='sub_api') +@hug.extend_api(sub_command="sub_api") def extend_with(): - return (sub_api, ) + return (sub_api,) -if __name__ == '__main__': +if __name__ == "__main__": hug.API(__name__).cli() diff --git a/examples/multi_file_cli/sub_api.py b/examples/multi_file_cli/sub_api.py index dd21eda0..20ffa319 100644 --- a/examples/multi_file_cli/sub_api.py +++ b/examples/multi_file_cli/sub_api.py @@ -3,4 +3,4 @@ @hug.cli() def hello(): - return 'Hello world' + return "Hello world" diff --git a/examples/multiple_files/api.py b/examples/multiple_files/api.py index f26c160b..e3a1b29f 100644 --- a/examples/multiple_files/api.py +++ b/examples/multiple_files/api.py @@ -3,7 +3,7 @@ import part_2 -@hug.get('/') +@hug.get("/") def say_hi(): """This view will be at the path ``/``""" return "Hi from root" diff --git a/examples/multiple_files/part_1.py b/examples/multiple_files/part_1.py index 50fb4a71..2a460865 100644 --- a/examples/multiple_files/part_1.py +++ b/examples/multiple_files/part_1.py @@ -4,4 +4,4 @@ @hug.get() def part1(): """This view will be at the path ``/part1``""" - return 'part1' + return "part1" diff --git a/examples/multiple_files/part_2.py b/examples/multiple_files/part_2.py index 4c8dfdad..350ce8f6 100644 --- a/examples/multiple_files/part_2.py +++ b/examples/multiple_files/part_2.py @@ -4,4 +4,4 @@ @hug.get() def part2(): """This view will be at the path ``/part2``""" - return 'Part 2' + return "Part 2" diff --git a/examples/override_404.py b/examples/override_404.py index 1b3371a8..261d6f14 100644 --- a/examples/override_404.py +++ b/examples/override_404.py @@ -3,9 +3,9 @@ @hug.get() def hello_world(): - return 'Hello world!' + return "Hello world!" @hug.not_found() def not_found(): - return {'Nothing': 'to see'} + return {"Nothing": "to see"} diff --git a/examples/pil_example/pill.py b/examples/pil_example/pill.py index 910c6e52..3f9a8a67 100644 --- a/examples/pil_example/pill.py +++ b/examples/pil_example/pill.py @@ -2,8 +2,8 @@ from PIL import Image, ImageDraw -@hug.get('/image.png', output=hug.output_format.png_image) +@hug.get("/image.png", output=hug.output_format.png_image) def create_image(): - image = Image.new('RGB', (100, 50)) # create the image - ImageDraw.Draw(image).text((10, 10), 'Hello World!', fill=(255, 0, 0)) + image = Image.new("RGB", (100, 50)) # create the image + ImageDraw.Draw(image).text((10, 10), "Hello World!", fill=(255, 0, 0)) return image diff --git a/examples/quick_server.py b/examples/quick_server.py index 423158ec..5fe9b1bf 100644 --- a/examples/quick_server.py +++ b/examples/quick_server.py @@ -3,8 +3,8 @@ @hug.get() def quick(): - return 'Serving!' + return "Serving!" -if __name__ == '__main__': +if __name__ == "__main__": hug.API(__name__).http.serve() diff --git a/examples/quick_start/first_step_1.py b/examples/quick_start/first_step_1.py index 3cc8dbea..971ded8f 100644 --- a/examples/quick_start/first_step_1.py +++ b/examples/quick_start/first_step_1.py @@ -5,5 +5,4 @@ @hug.local() def happy_birthday(name: hug.types.text, age: hug.types.number, hug_timer=3): """Says happy birthday to a user""" - return {'message': 'Happy {0} Birthday {1}!'.format(age, name), - 'took': float(hug_timer)} + return {"message": "Happy {0} Birthday {1}!".format(age, name), "took": float(hug_timer)} diff --git a/examples/quick_start/first_step_2.py b/examples/quick_start/first_step_2.py index e24b12de..ee314ae5 100644 --- a/examples/quick_start/first_step_2.py +++ b/examples/quick_start/first_step_2.py @@ -2,9 +2,8 @@ import hug -@hug.get(examples='name=Timothy&age=26') +@hug.get(examples="name=Timothy&age=26") @hug.local() def happy_birthday(name: hug.types.text, age: hug.types.number, hug_timer=3): """Says happy birthday to a user""" - return {'message': 'Happy {0} Birthday {1}!'.format(age, name), - 'took': float(hug_timer)} + return {"message": "Happy {0} Birthday {1}!".format(age, name), "took": float(hug_timer)} diff --git a/examples/quick_start/first_step_3.py b/examples/quick_start/first_step_3.py index 8f169dc5..33a02c92 100644 --- a/examples/quick_start/first_step_3.py +++ b/examples/quick_start/first_step_3.py @@ -3,13 +3,12 @@ @hug.cli() -@hug.get(examples='name=Timothy&age=26') +@hug.get(examples="name=Timothy&age=26") @hug.local() def happy_birthday(name: hug.types.text, age: hug.types.number, hug_timer=3): """Says happy birthday to a user""" - return {'message': 'Happy {0} Birthday {1}!'.format(age, name), - 'took': float(hug_timer)} + return {"message": "Happy {0} Birthday {1}!".format(age, name), "took": float(hug_timer)} -if __name__ == '__main__': +if __name__ == "__main__": happy_birthday.interface.cli() diff --git a/examples/return_400.py b/examples/return_400.py index 9129a440..859d4c09 100644 --- a/examples/return_400.py +++ b/examples/return_400.py @@ -1,6 +1,7 @@ import hug from falcon import HTTP_400 + @hug.get() def only_positive(positive: int, response): if positive < 0: diff --git a/examples/secure_auth_with_db_example.py b/examples/secure_auth_with_db_example.py index f2d3922a..e1ce207e 100644 --- a/examples/secure_auth_with_db_example.py +++ b/examples/secure_auth_with_db_example.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -db = TinyDB('db.json') +db = TinyDB("db.json") """ Helper Methods @@ -21,8 +21,8 @@ def hash_password(password, salt): :param salt: :return: Hex encoded SHA512 hash of provided password """ - password = str(password).encode('utf-8') - salt = str(salt).encode('utf-8') + password = str(password).encode("utf-8") + salt = str(salt).encode("utf-8") return hashlib.sha512(password + salt).hexdigest() @@ -32,7 +32,7 @@ def gen_api_key(username): :param username: :return: Hex encoded SHA512 random string """ - salt = str(os.urandom(64)).encode('utf-8') + salt = str(os.urandom(64)).encode("utf-8") return hash_password(username, salt) @@ -51,8 +51,8 @@ def authenticate_user(username, password): logger.warning("User %s not found", username) return False - if user['password'] == hash_password(password, user.get('salt')): - return user['username'] + if user["password"] == hash_password(password, user.get("salt")): + return user["username"] return False @@ -67,9 +67,10 @@ def authenticate_key(api_key): user_model = Query() user = db.search(user_model.api_key == api_key)[0] if user: - return user['username'] + return user["username"] return False + """ API Methods start here """ @@ -89,30 +90,19 @@ def add_user(username, password): user_model = Query() if db.search(user_model.username == username): - return { - 'error': 'User {0} already exists'.format(username) - } + return {"error": "User {0} already exists".format(username)} - salt = hashlib.sha512(str(os.urandom(64)).encode('utf-8')).hexdigest() + salt = hashlib.sha512(str(os.urandom(64)).encode("utf-8")).hexdigest() password = hash_password(password, salt) api_key = gen_api_key(username) - user = { - 'username': username, - 'password': password, - 'salt': salt, - 'api_key': api_key - } + user = {"username": username, "password": password, "salt": salt, "api_key": api_key} user_id = db.insert(user) - return { - 'result': 'success', - 'eid': user_id, - 'user_created': user - } + return {"result": "success", "eid": user_id, "user_created": user} -@hug.get('/api/get_api_key', requires=basic_authentication) +@hug.get("/api/get_api_key", requires=basic_authentication) def get_token(authed_user: hug.directives.user): """ Get Job details @@ -123,34 +113,26 @@ def get_token(authed_user: hug.directives.user): user = db.search(user_model.username == authed_user)[0] if user: - out = { - 'user': user['username'], - 'api_key': user['api_key'] - } + out = {"user": user["username"], "api_key": user["api_key"]} else: # this should never happen - out = { - 'error': 'User {0} does not exist'.format(authed_user) - } + out = {"error": "User {0} does not exist".format(authed_user)} return out # Same thing, but authenticating against an API key -@hug.get(('/api/job', '/api/job/{job_id}/'), requires=api_key_authentication) +@hug.get(("/api/job", "/api/job/{job_id}/"), requires=api_key_authentication) def get_job_details(job_id): """ Get Job details :param job_id: :return: """ - job = { - 'job_id': job_id, - 'details': 'Details go here' - } + job = {"job_id": job_id, "details": "Details go here"} return job -if __name__ == '__main__': +if __name__ == "__main__": add_user.interface.cli() diff --git a/examples/sink_example.py b/examples/sink_example.py index 7b2614fa..aadd395f 100644 --- a/examples/sink_example.py +++ b/examples/sink_example.py @@ -6,6 +6,6 @@ import hug -@hug.sink('/all') +@hug.sink("/all") def my_sink(request): - return request.path.replace('/all', '') + return request.path.replace("/all", "") diff --git a/examples/smtp_envelope_example.py b/examples/smtp_envelope_example.py index 9c1c6b82..ffa1db51 100644 --- a/examples/smtp_envelope_example.py +++ b/examples/smtp_envelope_example.py @@ -4,9 +4,8 @@ @hug.directive() class SMTP(object): - def __init__(self, *args, **kwargs): - self.smtp = envelopes.SMTP(host='127.0.0.1') + self.smtp = envelopes.SMTP(host="127.0.0.1") self.envelopes_to_send = list() def send_envelope(self, envelope): @@ -19,12 +18,12 @@ def cleanup(self, exception=None): self.smtp.send(envelope) -@hug.get('/hello') +@hug.get("/hello") def send_hello_email(smtp: SMTP): envelope = envelopes.Envelope( - from_addr=(u'me@example.com', u'From me'), - to_addr=(u'world@example.com', u'To World'), - subject=u'Hello', - text_body=u"World!" + from_addr=(u"me@example.com", u"From me"), + to_addr=(u"world@example.com", u"To World"), + subject=u"Hello", + text_body=u"World!", ) smtp.send_envelope(envelope) diff --git a/examples/sqlalchemy_example/demo/api.py b/examples/sqlalchemy_example/demo/api.py index 2e63b410..b0617535 100644 --- a/examples/sqlalchemy_example/demo/api.py +++ b/examples/sqlalchemy_example/demo/api.py @@ -6,40 +6,28 @@ from demo.validation import CreateUserSchema, DumpSchema, unique_username -@hug.post('/create_user2', requires=basic_authentication) -def create_user2( - db: SqlalchemySession, - data: CreateUserSchema() -): - user = TestUser( - **data - ) +@hug.post("/create_user2", requires=basic_authentication) +def create_user2(db: SqlalchemySession, data: CreateUserSchema()): + user = TestUser(**data) db.add(user) db.flush() return dict() -@hug.post('/create_user', requires=basic_authentication) -def create_user( - db: SqlalchemySession, - username: unique_username, - password: hug.types.text -): - user = TestUser( - username=username, - password=password - ) +@hug.post("/create_user", requires=basic_authentication) +def create_user(db: SqlalchemySession, username: unique_username, password: hug.types.text): + user = TestUser(username=username, password=password) db.add(user) db.flush() return dict() -@hug.get('/test') +@hug.get("/test") def test(): - return '' + return "" -@hug.get('/hello') +@hug.get("/hello") def make_simple_query(db: SqlalchemySession): for word in ["hello", "world", ":)"]: test_model = TestModel() @@ -49,7 +37,7 @@ def make_simple_query(db: SqlalchemySession): return " ".join([obj.name for obj in db.query(TestModel).all()]) -@hug.get('/hello2') +@hug.get("/hello2") def transform_example(db: SqlalchemySession) -> DumpSchema(): for word in ["hello", "world", ":)"]: test_model = TestModel() @@ -59,6 +47,6 @@ def transform_example(db: SqlalchemySession) -> DumpSchema(): return dict(users=db.query(TestModel).all()) -@hug.get('/protected', requires=basic_authentication) +@hug.get("/protected", requires=basic_authentication) def protected(): - return 'smile :)' + return "smile :)" diff --git a/examples/sqlalchemy_example/demo/app.py b/examples/sqlalchemy_example/demo/app.py index 321f6052..748ca17d 100644 --- a/examples/sqlalchemy_example/demo/app.py +++ b/examples/sqlalchemy_example/demo/app.py @@ -19,7 +19,7 @@ def delete_context(context: SqlalchemyContext, exception=None, errors=None, lack @hug.local(skip_directives=False) def initialize(db: SqlalchemySession): - admin = TestUser(username='admin', password='admin') + admin = TestUser(username="admin", password="admin") db.add(admin) db.flush() diff --git a/examples/sqlalchemy_example/demo/authentication.py b/examples/sqlalchemy_example/demo/authentication.py index 1cd1a447..09aabb6d 100644 --- a/examples/sqlalchemy_example/demo/authentication.py +++ b/examples/sqlalchemy_example/demo/authentication.py @@ -7,8 +7,7 @@ @hug.authentication.basic def basic_authentication(username, password, context: SqlalchemyContext): return context.db.query( - context.db.query(TestUser).filter( - TestUser.username == username, - TestUser.password == password - ).exists() + context.db.query(TestUser) + .filter(TestUser.username == username, TestUser.password == password) + .exists() ).scalar() diff --git a/examples/sqlalchemy_example/demo/context.py b/examples/sqlalchemy_example/demo/context.py index 46d5661a..e20a3126 100644 --- a/examples/sqlalchemy_example/demo/context.py +++ b/examples/sqlalchemy_example/demo/context.py @@ -10,7 +10,6 @@ class SqlalchemyContext(object): - def __init__(self): self._db = session_factory() diff --git a/examples/sqlalchemy_example/demo/directives.py b/examples/sqlalchemy_example/demo/directives.py index 921e287e..3f430133 100644 --- a/examples/sqlalchemy_example/demo/directives.py +++ b/examples/sqlalchemy_example/demo/directives.py @@ -6,6 +6,5 @@ @hug.directive() class SqlalchemySession(Session): - - def __new__(cls, *args, context: SqlalchemyContext=None, **kwargs): + def __new__(cls, *args, context: SqlalchemyContext = None, **kwargs): return context.db diff --git a/examples/sqlalchemy_example/demo/models.py b/examples/sqlalchemy_example/demo/models.py index ceb40a3b..25b2445f 100644 --- a/examples/sqlalchemy_example/demo/models.py +++ b/examples/sqlalchemy_example/demo/models.py @@ -5,13 +5,15 @@ class TestModel(Base): - __tablename__ = 'test_model' + __tablename__ = "test_model" id = Column(Integer, primary_key=True) name = Column(String) class TestUser(Base): - __tablename__ = 'test_user' + __tablename__ = "test_user" id = Column(Integer, primary_key=True) username = Column(String) - password = Column(String) # do not store plain password in the database, hash it, see porridge for example + password = Column( + String + ) # do not store plain password in the database, hash it, see porridge for example diff --git a/examples/sqlalchemy_example/demo/validation.py b/examples/sqlalchemy_example/demo/validation.py index c8371162..c54f0711 100644 --- a/examples/sqlalchemy_example/demo/validation.py +++ b/examples/sqlalchemy_example/demo/validation.py @@ -11,11 +11,9 @@ @hug.type(extend=hug.types.text, chain=True, accept_context=True) def unique_username(value, context: SqlalchemyContext): if context.db.query( - context.db.query(TestUser).filter( - TestUser.username == value - ).exists() + context.db.query(TestUser).filter(TestUser.username == value).exists() ).scalar(): - raise ValueError('User with a username {0} already exists.'.format(value)) + raise ValueError("User with a username {0} already exists.".format(value)) return value @@ -26,22 +24,19 @@ class CreateUserSchema(Schema): @validates_schema def check_unique_username(self, data): if self.context.db.query( - self.context.db.query(TestUser).filter( - TestUser.username == data['username'] - ).exists() + self.context.db.query(TestUser).filter(TestUser.username == data["username"]).exists() ).scalar(): - raise ValueError('User with a username {0} already exists.'.format(data['username'])) + raise ValueError("User with a username {0} already exists.".format(data["username"])) class DumpUserSchema(ModelSchema): - @property def session(self): return self.context.db class Meta: model = TestModel - fields = ('name',) + fields = ("name",) class DumpSchema(Schema): diff --git a/examples/static_serve.py b/examples/static_serve.py index 126b06e3..eac7019a 100644 --- a/examples/static_serve.py +++ b/examples/static_serve.py @@ -9,6 +9,7 @@ tmp_dir_object = None + def setup(api=None): """Sets up and fills test directory for serving. @@ -28,13 +29,16 @@ def setup(api=None): # populate directory a with text files file_list = [ - ["hi.txt", """Hi World!"""], - ["hi.html", """Hi World!"""], - ["hello.html", """ + ["hi.txt", """Hi World!"""], + ["hi.html", """Hi World!"""], + [ + "hello.html", + """ pop-up - """], - ["hi.js", """alert('Hi World')""" ] + """, + ], + ["hi.js", """alert('Hi World')"""], ] for f in file_list: @@ -42,16 +46,16 @@ def setup(api=None): fo.write(f[1]) # populate directory b with binary file - image = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\n\x00\x00\x00\n\x08\x02\x00\x00\x00\x02PX\xea\x00\x00\x006IDAT\x18\xd3c\xfc\xff\xff?\x03n\xc0\xc4\x80\x170100022222\xc2\x85\x90\xb9\x04t3\x92`7\xb2\x15D\xeb\xc6\xe34\xa8n4c\xe1F\x120\x1c\x00\xc6z\x12\x1c\x8cT\xf2\x1e\x00\x00\x00\x00IEND\xaeB`\x82' + image = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\n\x00\x00\x00\n\x08\x02\x00\x00\x00\x02PX\xea\x00\x00\x006IDAT\x18\xd3c\xfc\xff\xff?\x03n\xc0\xc4\x80\x170100022222\xc2\x85\x90\xb9\x04t3\x92`7\xb2\x15D\xeb\xc6\xe34\xa8n4c\xe1F\x120\x1c\x00\xc6z\x12\x1c\x8cT\xf2\x1e\x00\x00\x00\x00IEND\xaeB`\x82" with open(os.path.join(dir_b, "smile.png"), mode="wb") as fo: - fo.write(image) + fo.write(image) -@hug.static('/static') +@hug.static("/static") def my_static_dirs(): """Returns static directory names to be served.""" global tmp_dir_object if tmp_dir_object == None: setup() - return(tmp_dir_object.name,) + return (tmp_dir_object.name,) diff --git a/examples/streaming_movie_server/movie_server.py b/examples/streaming_movie_server/movie_server.py index b78bf693..443c83af 100644 --- a/examples/streaming_movie_server/movie_server.py +++ b/examples/streaming_movie_server/movie_server.py @@ -5,4 +5,4 @@ @hug.get(output=hug.output_format.mp4_video) def watch(): """Watch an example movie, streamed directly to you from hug""" - return 'movie.mp4' + return "movie.mp4" diff --git a/examples/test_happy_birthday.py b/examples/test_happy_birthday.py index ce3a165c..e7bb6c69 100644 --- a/examples/test_happy_birthday.py +++ b/examples/test_happy_birthday.py @@ -2,15 +2,17 @@ import happy_birthday from falcon import HTTP_400, HTTP_404, HTTP_200 + def tests_happy_birthday(): - response = hug.test.get(happy_birthday, 'happy_birthday', {'name': 'Timothy', 'age': 25}) + response = hug.test.get(happy_birthday, "happy_birthday", {"name": "Timothy", "age": 25}) assert response.status == HTTP_200 assert response.data is not None + def tests_season_greetings(): - response = hug.test.get(happy_birthday, 'greet/Christmas') + response = hug.test.get(happy_birthday, "greet/Christmas") assert response.status == HTTP_200 assert response.data is not None assert str(response.data) == "Merry Christmas!" - response = hug.test.get(happy_birthday, 'greet/holidays') + response = hug.test.get(happy_birthday, "greet/holidays") assert str(response.data) == "Happy holidays!" diff --git a/examples/unicode_output.py b/examples/unicode_output.py index e48a5c5e..f0a408ad 100644 --- a/examples/unicode_output.py +++ b/examples/unicode_output.py @@ -5,4 +5,4 @@ @hug.get() def unicode_response(): """An example endpoint that returns unicode data nested within the result object""" - return {'data': ['Τη γλώσσα μου έδωσαν ελληνική']} + return {"data": ["Τη γλώσσα μου έδωσαν ελληνική"]} diff --git a/examples/use_socket.py b/examples/use_socket.py index 129b0bcf..d64e52d8 100644 --- a/examples/use_socket.py +++ b/examples/use_socket.py @@ -5,22 +5,24 @@ import time -http_socket = hug.use.Socket(connect_to=('www.google.com', 80), proto='tcp', pool=4, timeout=10.0) -ntp_service = hug.use.Socket(connect_to=('127.0.0.1', 123), proto='udp', pool=4, timeout=10.0) +http_socket = hug.use.Socket(connect_to=("www.google.com", 80), proto="tcp", pool=4, timeout=10.0) +ntp_service = hug.use.Socket(connect_to=("127.0.0.1", 123), proto="udp", pool=4, timeout=10.0) EPOCH_START = 2208988800 + + @hug.get() def get_time(): """Get time from a locally running NTP server""" - time_request = '\x1b' + 47 * '\0' + time_request = "\x1b" + 47 * "\0" now = struct.unpack("!12I", ntp_service.request(time_request, timeout=5.0).data.read())[10] return time.ctime(now - EPOCH_START) @hug.get() -def reverse_http_proxy(length: int=100): +def reverse_http_proxy(length: int = 100): """Simple reverse http proxy function that returns data/html from another http server (via sockets) only drawback is the peername is static, and currently does not support being changed. Example: curl localhost:8000/reverse_http_proxy?length=400""" diff --git a/examples/versioning.py b/examples/versioning.py index 9c6a110d..c7b3004d 100644 --- a/examples/versioning.py +++ b/examples/versioning.py @@ -2,21 +2,21 @@ import hug -@hug.get('/echo', versions=1) +@hug.get("/echo", versions=1) def echo(text): return text -@hug.get('/echo', versions=range(2, 5)) # noqa +@hug.get("/echo", versions=range(2, 5)) # noqa def echo(text): - return 'Echo: {text}'.format(**locals()) + return "Echo: {text}".format(**locals()) -@hug.get('/unversioned') +@hug.get("/unversioned") def hello(): - return 'Hello world!' + return "Hello world!" -@hug.get('/echo', versions='6') +@hug.get("/echo", versions="6") def echo(text): - return 'Version 6' + return "Version 6" diff --git a/examples/write_once.py b/examples/write_once.py index 796d161e..b61ee09a 100644 --- a/examples/write_once.py +++ b/examples/write_once.py @@ -6,8 +6,8 @@ @hug.local() @hug.cli() @hug.get() -def top_post(section: hug.types.one_of(('news', 'newest', 'show'))='news'): +def top_post(section: hug.types.one_of(("news", "newest", "show")) = "news"): """Returns the top post from the provided section""" - content = requests.get('https://news.ycombinator.com/{0}'.format(section)).content - text = content.decode('utf-8') - return text.split('')[1].split("")[1].split("<")[0] + content = requests.get("https://news.ycombinator.com/{0}".format(section)).content + text = content.decode("utf-8") + return text.split("")[1].split("")[1].split("<")[0] diff --git a/hug/__init__.py b/hug/__init__.py index beb194e9..e40d8508 100644 --- a/hug/__init__.py +++ b/hug/__init__.py @@ -33,19 +33,66 @@ from falcon import * -from hug import (authentication, directives, exceptions, format, input_format, introspect, - middleware, output_format, redirect, route, test, transform, types, use, validate) +from hug import ( + authentication, + directives, + exceptions, + format, + input_format, + introspect, + middleware, + output_format, + redirect, + route, + test, + transform, + types, + use, + validate, +) from hug._version import current from hug.api import API -from hug.decorators import (context_factory, default_input_format, default_output_format, - delete_context, directive, extend_api, middleware_class, reqresp_middleware, - request_middleware, response_middleware, startup, wraps) -from hug.route import (call, cli, connect, delete, exception, get, get_post, head, http, local, - not_found, object, options, patch, post, put, sink, static, trace) +from hug.decorators import ( + context_factory, + default_input_format, + default_output_format, + delete_context, + directive, + extend_api, + middleware_class, + reqresp_middleware, + request_middleware, + response_middleware, + startup, + wraps, +) +from hug.route import ( + call, + cli, + connect, + delete, + exception, + get, + get_post, + head, + http, + local, + not_found, + object, + options, + patch, + post, + put, + sink, + static, + trace, +) from hug.types import create as type -from hug import development_runner # isort:skip -from hug import defaults # isort:skip - must be imported last for defaults to have access to all modules +from hug import development_runner # isort:skip +from hug import ( + defaults, +) # isort:skip - must be imported last for defaults to have access to all modules try: # pragma: no cover - defaulting to uvloop if it is installed import uvloop diff --git a/hug/api.py b/hug/api.py index c20c8fe7..a2396ef9 100644 --- a/hug/api.py +++ b/hug/api.py @@ -60,14 +60,17 @@ Copyright (C) 2016 Timothy Edmund Crosley Under the MIT License -""".format(current) +""".format( + current +) class InterfaceAPI(object): """Defines the per-interface API which defines all shared information for a specific interface, and how it should be exposed """ - __slots__ = ('api', ) + + __slots__ = ("api",) def __init__(self, api): self.api = api @@ -75,10 +78,22 @@ def __init__(self, api): class HTTPInterfaceAPI(InterfaceAPI): """Defines the HTTP interface specific API""" - __slots__ = ('routes', 'versions', 'base_url', '_output_format', '_input_format', 'versioned', '_middleware', - '_not_found_handlers', 'sinks', '_not_found', '_exception_handlers') - def __init__(self, api, base_url=''): + __slots__ = ( + "routes", + "versions", + "base_url", + "_output_format", + "_input_format", + "versioned", + "_middleware", + "_not_found_handlers", + "sinks", + "_not_found", + "_exception_handlers", + ) + + def __init__(self, api, base_url=""): super().__init__(api) self.versions = set() self.routes = OrderedDict() @@ -88,7 +103,7 @@ def __init__(self, api, base_url=''): @property def output_format(self): - return getattr(self, '_output_format', hug.defaults.output_format) + return getattr(self, "_output_format", hug.defaults.output_format) @output_format.setter def output_format(self, formatter): @@ -97,7 +112,7 @@ def output_format(self, formatter): @property def not_found(self): """Returns the active not found handler""" - return getattr(self, '_not_found', self.base_404) + return getattr(self, "_not_found", self.base_404) def urls(self): """Returns a generator of all URLs attached to this API""" @@ -118,17 +133,19 @@ def handlers(self): def input_format(self, content_type): """Returns the set input_format handler for the given content_type""" - return getattr(self, '_input_format', {}).get(content_type, hug.defaults.input_format.get(content_type, None)) + return getattr(self, "_input_format", {}).get( + content_type, hug.defaults.input_format.get(content_type, None) + ) def set_input_format(self, content_type, handler): """Sets an input format handler for this Hug API, given the specified content_type""" - if getattr(self, '_input_format', None) is None: + if getattr(self, "_input_format", None) is None: self._input_format = {} self._input_format[content_type] = handler @property def middleware(self): - return getattr(self, '_middleware', None) + return getattr(self, "_middleware", None) def add_middleware(self, middleware): """Adds a middleware object used to process all incoming requests against the API""" @@ -142,20 +159,20 @@ def add_sink(self, sink, url, base_url=""): self.sinks[base_url][url] = sink def exception_handlers(self, version=None): - if not hasattr(self, '_exception_handlers'): + if not hasattr(self, "_exception_handlers"): return None return self._exception_handlers.get(version, self._exception_handlers.get(None, None)) - def add_exception_handler(self, exception_type, error_handler, versions=(None, )): + def add_exception_handler(self, exception_type, error_handler, versions=(None,)): """Adds a error handler to the hug api""" - versions = (versions, ) if not isinstance(versions, (tuple, list)) else versions - if not hasattr(self, '_exception_handlers'): + versions = (versions,) if not isinstance(versions, (tuple, list)) else versions + if not hasattr(self, "_exception_handlers"): self._exception_handlers = {} for version in versions: placement = self._exception_handlers.setdefault(version, OrderedDict()) - placement[exception_type] = (error_handler, ) + placement.get(exception_type, tuple()) + placement[exception_type] = (error_handler,) + placement.get(exception_type, tuple()) def extend(self, http_api, route="", base_url="", **kwargs): """Adds handlers from a different Hug API to this one - to create a single API""" @@ -174,18 +191,18 @@ def extend(self, http_api, route="", base_url="", **kwargs): for url, sink in sinks.items(): self.add_sink(sink, route + url, base_url=base_url) - for middleware in (http_api.middleware or ()): + for middleware in http_api.middleware or (): self.add_middleware(middleware) - for version, handler in getattr(http_api, '_exception_handlers', {}).items(): + for version, handler in getattr(http_api, "_exception_handlers", {}).items(): for exception_type, exception_handlers in handler.items(): target_exception_handlers = self.exception_handlers(version) or {} for exception_handler in exception_handlers: if exception_type not in target_exception_handlers: self.add_exception_handler(exception_type, exception_handler, version) - for input_format, input_format_handler in getattr(http_api, '_input_format', {}).items(): - if not input_format in getattr(self, '_input_format', {}): + for input_format, input_format_handler in getattr(http_api, "_input_format", {}).items(): + if not input_format in getattr(self, "_input_format", {}): self.set_input_format(input_format, input_format_handler) for version, handler in http_api.not_found_handlers.items(): @@ -194,7 +211,7 @@ def extend(self, http_api, route="", base_url="", **kwargs): @property def not_found_handlers(self): - return getattr(self, '_not_found_handlers', {}) + return getattr(self, "_not_found_handlers", {}) def set_not_found_handler(self, handler, version=None): """Sets the not_found handler for the specified version of the api""" @@ -209,7 +226,7 @@ def documentation(self, base_url=None, api_version=None, prefix=""): base_url = self.base_url if base_url is None else base_url overview = self.api.doc if overview: - documentation['overview'] = overview + documentation["overview"] = overview version_dict = OrderedDict() versions = self.versions @@ -220,33 +237,38 @@ def documentation(self, base_url=None, api_version=None, prefix=""): versions_list.remove(False) if api_version is None and len(versions_list) > 0: api_version = max(versions_list) - documentation['version'] = api_version + documentation["version"] = api_version elif api_version is not None: - documentation['version'] = api_version + documentation["version"] = api_version if versions_list: - documentation['versions'] = versions_list + documentation["versions"] = versions_list for router_base_url, routes in self.routes.items(): for url, methods in routes.items(): for method, method_versions in methods.items(): for version, handler in method_versions.items(): - if getattr(handler, 'private', False): + if getattr(handler, "private", False): continue if version is None: applies_to = versions else: - applies_to = (version, ) + applies_to = (version,) for version in applies_to: if api_version and version != api_version: continue if base_url and router_base_url != base_url: continue doc = version_dict.setdefault(url, OrderedDict()) - doc[method] = handler.documentation(doc.get(method, None), version=version, prefix=prefix, - base_url=router_base_url, url=url) - documentation['handlers'] = version_dict + doc[method] = handler.documentation( + doc.get(method, None), + version=version, + prefix=prefix, + base_url=router_base_url, + url=url, + ) + documentation["handlers"] = version_dict return documentation - def serve(self, host='', port=8000, no_documentation=False, display_intro=True): + def serve(self, host="", port=8000, no_documentation=False, display_intro=True): """Runs the basic hug development server against this API""" if no_documentation: api = self.server(None) @@ -282,14 +304,14 @@ def determine_version(self, request, api_version=None): if version_header: request_version.add(version_header) - version_param = request.get_param('api_version') + version_param = request.get_param("api_version") if version_param is not None: request_version.add(version_param) if len(request_version) > 1: - raise ValueError('You are requesting conflicting versions') + raise ValueError("You are requesting conflicting versions") - return next(iter(request_version or (None, ))) + return next(iter(request_version or (None,))) def documentation_404(self, base_url=None): """Returns a smart 404 page that contains documentation for the written API""" @@ -301,14 +323,17 @@ def handle_404(request, response, *args, **kwargs): url_prefix = request.forwarded_uri.split(request.path)[0] to_return = OrderedDict() - to_return['404'] = ("The API call you tried to make was not defined. " - "Here's a definition of the API to help you get going :)") - to_return['documentation'] = self.documentation(base_url, self.determine_version(request, False), - prefix=url_prefix) + to_return["404"] = ( + "The API call you tried to make was not defined. " + "Here's a definition of the API to help you get going :)" + ) + to_return["documentation"] = self.documentation( + base_url, self.determine_version(request, False), prefix=url_prefix + ) if self.output_format == hug.output_format.json: - response.data = hug.output_format.json(to_return, indent=4, separators=(',', ': ')) - response.content_type = 'application/json; charset=utf-8' + response.data = hug.output_format.json(to_return, indent=4, separators=(",", ": ")) + response.content_type = "application/json; charset=utf-8" else: response.data = self.output_format(to_return, request=request, response=response) response.content_type = self.output_format.content_type @@ -318,14 +343,16 @@ def handle_404(request, response, *args, **kwargs): handle_404.interface = True return handle_404 - def version_router(self, request, response, api_version=None, versions={}, not_found=None, **kwargs): + def version_router( + self, request, response, api_version=None, versions={}, not_found=None, **kwargs + ): """Intelligently routes a request to the correct handler based on the version being requested""" request_version = self.determine_version(request, api_version) if request_version: request_version = int(request_version) - versions.get(request_version or False, versions.get(None, not_found))(request, response, - api_version=api_version, - **kwargs) + versions.get(request_version or False, versions.get(None, not_found))( + request, response, api_version=api_version, **kwargs + ) def server(self, default_not_found=True, base_url=None): """Returns a WSGI compatible API server for the given Hug API module""" @@ -339,8 +366,12 @@ def server(self, default_not_found=True, base_url=None): if len(self.not_found_handlers) == 1 and None in self.not_found_handlers: not_found_handler = self.not_found_handlers[None] else: - not_found_handler = partial(self.version_router, api_version=False, - versions=self.not_found_handlers, not_found=default_not_found) + not_found_handler = partial( + self.version_router, + api_version=False, + versions=self.not_found_handlers, + not_found=default_not_found, + ) not_found_handler.interface = True if not_found_handler: @@ -349,7 +380,7 @@ def server(self, default_not_found=True, base_url=None): for sink_base_url, sinks in self.sinks.items(): for url, extra_sink in sinks.items(): - falcon_api.add_sink(extra_sink, sink_base_url + url + '(?P.*)') + falcon_api.add_sink(extra_sink, sink_base_url + url + "(?P.*)") for router_base_url, routes in self.routes.items(): for url, methods in routes.items(): @@ -359,30 +390,34 @@ def server(self, default_not_found=True, base_url=None): if len(versions) == 1 and None in versions.keys(): router[method_function] = versions[None] else: - router[method_function] = partial(self.version_router, versions=versions, - not_found=not_found_handler) + router[method_function] = partial( + self.version_router, versions=versions, not_found=not_found_handler + ) - router = namedtuple('Router', router.keys())(**router) + router = namedtuple("Router", router.keys())(**router) falcon_api.add_route(router_base_url + url, router) - if self.versions and self.versions != (None, ): - falcon_api.add_route(router_base_url + '/v{api_version}' + url, router) + if self.versions and self.versions != (None,): + falcon_api.add_route(router_base_url + "/v{api_version}" + url, router) def error_serializer(request, response, error): response.content_type = self.output_format.content_type - response.body = self.output_format({"errors": {error.title: error.description}}, - request, response) + response.body = self.output_format( + {"errors": {error.title: error.description}}, request, response + ) falcon_api.set_error_serializer(error_serializer) return falcon_api + HTTPInterfaceAPI.base_404.interface = True class CLIInterfaceAPI(InterfaceAPI): """Defines the CLI interface specific API""" - __slots__ = ('commands', 'error_exit_codes', '_output_format') - def __init__(self, api, version='', error_exit_codes=False): + __slots__ = ("commands", "error_exit_codes", "_output_format") + + def __init__(self, api, version="", error_exit_codes=False): super().__init__(api) self.commands = {} self.error_exit_codes = error_exit_codes @@ -398,7 +433,7 @@ def __call__(self, args=None): command = args.pop(1) result = self.commands.get(command)() - if self.error_exit_codes and bool(strtobool(result.decode('utf-8'))) is False: + if self.error_exit_codes and bool(strtobool(result.decode("utf-8"))) is False: sys.exit(1) def handlers(self): @@ -408,7 +443,9 @@ def handlers(self): def extend(self, cli_api, command_prefix="", sub_command="", **kwargs): """Extends this CLI api with the commands present in the provided cli_api object""" if sub_command and command_prefix: - raise ValueError('It is not currently supported to provide both a command_prefix and sub_command') + raise ValueError( + "It is not currently supported to provide both a command_prefix and sub_command" + ) if sub_command: self.commands[sub_command] = cli_api @@ -418,15 +455,16 @@ def extend(self, cli_api, command_prefix="", sub_command="", **kwargs): @property def output_format(self): - return getattr(self, '_output_format', hug.defaults.cli_output_format) + return getattr(self, "_output_format", hug.defaults.cli_output_format) @output_format.setter def output_format(self, formatter): self._output_format = formatter def __str__(self): - return "{0}\n\nAvailable Commands:{1}\n".format(self.api.doc or self.api.name, - "\n\n\t- " + "\n\t- ".join(self.commands.keys())) + return "{0}\n\nAvailable Commands:{1}\n".format( + self.api.doc or self.api.name, "\n\n\t- " + "\n\t- ".join(self.commands.keys()) + ) class ModuleSingleton(type): @@ -443,9 +481,10 @@ def __call__(cls, module=None, *args, **kwargs): elif module is None: return super().__call__(*args, **kwargs) - if not '__hug__' in module.__dict__: + if not "__hug__" in module.__dict__: + def api_auto_instantiate(*args, **kwargs): - if not hasattr(module, '__hug_serving__'): + if not hasattr(module, "__hug_serving__"): module.__hug_wsgi__ = module.__hug__.http.server() module.__hug_serving__ = True return module.__hug_wsgi__(*args, **kwargs) @@ -457,14 +496,27 @@ def api_auto_instantiate(*args, **kwargs): class API(object, metaclass=ModuleSingleton): """Stores the information necessary to expose API calls within this module externally""" - __slots__ = ('module', '_directives', '_http', '_cli', '_context', '_context_factory', '_delete_context', - '_startup_handlers', 'started', 'name', 'doc', 'cli_error_exit_codes') - def __init__(self, module=None, name='', doc='', cli_error_exit_codes=False): + __slots__ = ( + "module", + "_directives", + "_http", + "_cli", + "_context", + "_context_factory", + "_delete_context", + "_startup_handlers", + "started", + "name", + "doc", + "cli_error_exit_codes", + ) + + def __init__(self, module=None, name="", doc="", cli_error_exit_codes=False): self.module = module if module: - self.name = name or module.__name__ or '' - self.doc = doc or module.__doc__ or '' + self.name = name or module.__name__ or "" + self.doc = doc or module.__doc__ or "" else: self.name = name self.doc = doc @@ -473,39 +525,45 @@ def __init__(self, module=None, name='', doc='', cli_error_exit_codes=False): def directives(self): """Returns all directives applicable to this Hug API""" - directive_sources = chain(hug.defaults.directives.items(), getattr(self, '_directives', {}).items()) - return {'hug_' + directive_name: directive for directive_name, directive in directive_sources} + directive_sources = chain( + hug.defaults.directives.items(), getattr(self, "_directives", {}).items() + ) + return { + "hug_" + directive_name: directive for directive_name, directive in directive_sources + } def directive(self, name, default=None): """Returns the loaded directive with the specified name, or default if passed name is not present""" - return getattr(self, '_directives', {}).get(name, hug.defaults.directives.get(name, default)) + return getattr(self, "_directives", {}).get( + name, hug.defaults.directives.get(name, default) + ) def add_directive(self, directive): - self._directives = getattr(self, '_directives', {}) + self._directives = getattr(self, "_directives", {}) self._directives[directive.__name__] = directive def handlers(self): """Returns all registered handlers attached to this API""" - if getattr(self, '_http'): + if getattr(self, "_http"): yield from self.http.handlers() - if getattr(self, '_cli'): + if getattr(self, "_cli"): yield from self.cli.handlers() @property def http(self): - if not hasattr(self, '_http'): + if not hasattr(self, "_http"): self._http = HTTPInterfaceAPI(self) return self._http @property def cli(self): - if not hasattr(self, '_cli'): + if not hasattr(self, "_cli"): self._cli = CLIInterfaceAPI(self, error_exit_codes=self.cli_error_exit_codes) return self._cli @property def context_factory(self): - return getattr(self, '_context_factory', hug.defaults.context_factory) + return getattr(self, "_context_factory", hug.defaults.context_factory) @context_factory.setter def context_factory(self, context_factory_): @@ -513,7 +571,7 @@ def context_factory(self, context_factory_): @property def delete_context(self): - return getattr(self, '_delete_context', hug.defaults.delete_context) + return getattr(self, "_delete_context", hug.defaults.delete_context) @delete_context.setter def delete_context(self, delete_context_): @@ -521,7 +579,7 @@ def delete_context(self, delete_context_): @property def context(self): - if not hasattr(self, '_context'): + if not hasattr(self, "_context"): self._context = {} return self._context @@ -529,16 +587,16 @@ def extend(self, api, route="", base_url="", http=True, cli=True, **kwargs): """Adds handlers from a different Hug API to this one - to create a single API""" api = API(api) - if http and hasattr(api, '_http'): + if http and hasattr(api, "_http"): self.http.extend(api.http, route, base_url, **kwargs) - if cli and hasattr(api, '_cli'): + if cli and hasattr(api, "_cli"): self.cli.extend(api.cli, **kwargs) - for directive in getattr(api, '_directives', {}).values(): + for directive in getattr(api, "_directives", {}).values(): self.add_directive(directive) - for startup_handler in (api.startup_handlers or ()): + for startup_handler in api.startup_handlers or (): self.add_startup_handler(startup_handler) def add_startup_handler(self, handler): @@ -551,18 +609,23 @@ def add_startup_handler(self, handler): def _ensure_started(self): """Marks the API as started and runs all startup handlers""" if not self.started: - async_handlers = [startup_handler for startup_handler in self.startup_handlers if - introspect.is_coroutine(startup_handler)] + async_handlers = [ + startup_handler + for startup_handler in self.startup_handlers + if introspect.is_coroutine(startup_handler) + ] if async_handlers: loop = asyncio.get_event_loop() - loop.run_until_complete(asyncio.gather(*[handler(self) for handler in async_handlers], loop=loop)) + loop.run_until_complete( + asyncio.gather(*[handler(self) for handler in async_handlers], loop=loop) + ) for startup_handler in self.startup_handlers: if not startup_handler in async_handlers: startup_handler(self) @property def startup_handlers(self): - return getattr(self, '_startup_handlers', ()) + return getattr(self, "_startup_handlers", ()) def from_object(obj): diff --git a/hug/authentication.py b/hug/authentication.py index b331cb1e..f68d3ea5 100644 --- a/hug/authentication.py +++ b/hug/authentication.py @@ -33,7 +33,7 @@ def authenticator(function, challenges=()): The verify_user function passed in should accept an API key and return a user object to store in the request context if authentication succeeded. """ - challenges = challenges or ('{} realm="simple"'.format(function.__name__), ) + challenges = challenges or ('{} realm="simple"'.format(function.__name__),) def wrapper(verify_user): def authenticate(request, response, **kwargs): @@ -46,16 +46,20 @@ def authenticator_name(): return function.__name__ if result is None: - raise HTTPUnauthorized('Authentication Required', - 'Please provide valid {0} credentials'.format(authenticator_name()), - challenges=challenges) + raise HTTPUnauthorized( + "Authentication Required", + "Please provide valid {0} credentials".format(authenticator_name()), + challenges=challenges, + ) if result is False: - raise HTTPUnauthorized('Invalid Authentication', - 'Provided {0} credentials were invalid'.format(authenticator_name()), - challenges=challenges) + raise HTTPUnauthorized( + "Invalid Authentication", + "Provided {0} credentials were invalid".format(authenticator_name()), + challenges=challenges, + ) - request.context['user'] = result + request.context["user"] = result return True authenticate.__doc__ = function.__doc__ @@ -65,36 +69,42 @@ def authenticator_name(): @authenticator -def basic(request, response, verify_user, realm='simple', context=None, **kwargs): +def basic(request, response, verify_user, realm="simple", context=None, **kwargs): """Basic HTTP Authentication""" http_auth = request.auth - response.set_header('WWW-Authenticate', 'Basic') + response.set_header("WWW-Authenticate", "Basic") if http_auth is None: return if isinstance(http_auth, bytes): - http_auth = http_auth.decode('utf8') + http_auth = http_auth.decode("utf8") try: - auth_type, user_and_key = http_auth.split(' ', 1) + auth_type, user_and_key = http_auth.split(" ", 1) except ValueError: - raise HTTPUnauthorized('Authentication Error', - 'Authentication header is improperly formed', - challenges=('Basic realm="{}"'.format(realm), )) + raise HTTPUnauthorized( + "Authentication Error", + "Authentication header is improperly formed", + challenges=('Basic realm="{}"'.format(realm),), + ) - if auth_type.lower() == 'basic': + if auth_type.lower() == "basic": try: - user_id, key = base64.decodebytes(bytes(user_and_key.strip(), 'utf8')).decode('utf8').split(':', 1) + user_id, key = ( + base64.decodebytes(bytes(user_and_key.strip(), "utf8")).decode("utf8").split(":", 1) + ) try: user = verify_user(user_id, key) except TypeError: user = verify_user(user_id, key, context) if user: - response.set_header('WWW-Authenticate', '') + response.set_header("WWW-Authenticate", "") return user except (binascii.Error, ValueError): - raise HTTPUnauthorized('Authentication Error', - 'Unable to determine user and password with provided encoding', - challenges=('Basic realm="{}"'.format(realm), )) + raise HTTPUnauthorized( + "Authentication Error", + "Unable to determine user and password with provided encoding", + challenges=('Basic realm="{}"'.format(realm),), + ) return False @@ -106,7 +116,7 @@ def api_key(request, response, verify_user, context=None, **kwargs): API key as input, and return a user object to store in the request context if the request was successful. """ - api_key = request.get_header('X-Api-Key') + api_key = request.get_header("X-Api-Key") if api_key: try: @@ -127,7 +137,7 @@ def token(request, response, verify_user, context=None, **kwargs): Checks for the Authorization header and verifies using the verify_user function """ - token = request.get_header('Authorization') + token = request.get_header("Authorization") if token: try: verified_token = verify_user(token) @@ -142,8 +152,10 @@ def token(request, response, verify_user, context=None, **kwargs): def verify(user, password): """Returns a simple verification callback that simply verifies that the users and password match that provided""" + def verify_user(user_name, user_password): if user_name == user and user_password == password: return user_name return False + return verify_user diff --git a/hug/decorators.py b/hug/decorators.py index 0f749bbe..e58ee299 100644 --- a/hug/decorators.py +++ b/hug/decorators.py @@ -38,8 +38,11 @@ from hug.format import underscore -def default_output_format(content_type='application/json', apply_globally=False, api=None, cli=False, http=True): +def default_output_format( + content_type="application/json", apply_globally=False, api=None, cli=False, http=True +): """A decorator that allows you to override the default output format for an API""" + def decorator(formatter): formatter = hug.output_format.content_type(content_type)(formatter) if apply_globally: @@ -54,11 +57,13 @@ def decorator(formatter): if cli: apply_to_api.cli.output_format = formatter return formatter + return decorator -def default_input_format(content_type='application/json', apply_globally=False, api=None): +def default_input_format(content_type="application/json", apply_globally=False, api=None): """A decorator that allows you to override the default output format for an API""" + def decorator(formatter): formatter = hug.output_format.content_type(content_type)(formatter) if apply_globally: @@ -67,11 +72,13 @@ def decorator(formatter): apply_to_api = hug.API(api) if api else hug.api.from_object(formatter) apply_to_api.http.set_input_format(content_type, formatter) return formatter + return decorator def directive(apply_globally=False, api=None): """A decorator that registers a single hug directive""" + def decorator(directive_method): if apply_globally: hug.defaults.directives[underscore(directive_method.__name__)] = directive_method @@ -80,11 +87,13 @@ def decorator(directive_method): apply_to_api.add_directive(directive_method) directive_method.directive = True return directive_method + return decorator def context_factory(apply_globally=False, api=None): """A decorator that registers a single hug context factory""" + def decorator(context_factory_): if apply_globally: hug.defaults.context_factory = context_factory_ @@ -92,11 +101,13 @@ def decorator(context_factory_): apply_to_api = hug.API(api) if api else hug.api.from_object(context_factory_) apply_to_api.context_factory = context_factory_ return context_factory_ + return decorator def delete_context(apply_globally=False, api=None): """A decorator that registers a single hug delete context function""" + def decorator(delete_context_): if apply_globally: hug.defaults.delete_context = delete_context_ @@ -104,20 +115,24 @@ def decorator(delete_context_): apply_to_api = hug.API(api) if api else hug.api.from_object(delete_context_) apply_to_api.delete_context = delete_context_ return delete_context_ + return decorator def startup(api=None): """Runs the provided function on startup, passing in an instance of the api""" + def startup_wrapper(startup_function): apply_to_api = hug.API(api) if api else hug.api.from_object(startup_function) apply_to_api.add_startup_handler(startup_function) return startup_function + return startup_wrapper def request_middleware(api=None): """Registers a middleware function that will be called on every request""" + def decorator(middleware_method): apply_to_api = hug.API(api) if api else hug.api.from_object(middleware_method) @@ -129,11 +144,13 @@ def process_request(self, request, response): apply_to_api.http.add_middleware(MiddlewareRouter()) return middleware_method + return decorator def response_middleware(api=None): """Registers a middleware function that will be called on every response""" + def decorator(middleware_method): apply_to_api = hug.API(api) if api else hug.api.from_object(middleware_method) @@ -145,15 +162,18 @@ def process_response(self, request, response, resource, req_succeeded): apply_to_api.http.add_middleware(MiddlewareRouter()) return middleware_method + return decorator + def reqresp_middleware(api=None): """Registers a middleware function that will be called on every request and response""" + def decorator(middleware_generator): apply_to_api = hug.API(api) if api else hug.api.from_object(middleware_generator) class MiddlewareRouter(object): - __slots__ = ('gen', ) + __slots__ = ("gen",) def process_request(self, request, response): self.gen = middleware_generator(request) @@ -164,37 +184,45 @@ def process_response(self, request, response, resource, req_succeeded): apply_to_api.http.add_middleware(MiddlewareRouter()) return middleware_generator + return decorator + def middleware_class(api=None): """Registers a middleware class""" + def decorator(middleware_class): apply_to_api = hug.API(api) if api else hug.api.from_object(middleware_class) apply_to_api.http.add_middleware(middleware_class()) return middleware_class + return decorator def extend_api(route="", api=None, base_url="", **kwargs): """Extends the current api, with handlers from an imported api. Optionally provide a route that prefixes access""" + def decorator(extend_with): apply_to_api = hug.API(api) if api else hug.api.from_object(extend_with) for extended_api in extend_with(): apply_to_api.extend(extended_api, route, base_url, **kwargs) return extend_with + return decorator def wraps(function): """Enables building decorators around functions used for hug routes without changing their function signature""" + def wrap(decorator): decorator = functools.wraps(function)(decorator) - if not hasattr(function, 'original'): + if not hasattr(function, "original"): decorator.original = function else: decorator.original = function.original - delattr(function, 'original') + delattr(function, "original") return decorator + return wrap @@ -205,4 +233,5 @@ def auto_kwargs(function): @wraps(function) def call_function(*args, **kwargs): return function(*args, **{key: value for key, value in kwargs.items() if key in supported}) + return call_function diff --git a/hug/defaults.py b/hug/defaults.py index 5607bd6d..d5c68b58 100644 --- a/hug/defaults.py +++ b/hug/defaults.py @@ -27,23 +27,23 @@ cli_output_format = hug.output_format.text input_format = { - 'application/json': hug.input_format.json, - 'application/x-www-form-urlencoded': hug.input_format.urlencoded, - 'multipart/form-data': hug.input_format.multipart, - 'text/plain': hug.input_format.text, - 'text/css': hug.input_format.text, - 'text/html': hug.input_format.text + "application/json": hug.input_format.json, + "application/x-www-form-urlencoded": hug.input_format.urlencoded, + "multipart/form-data": hug.input_format.multipart, + "text/plain": hug.input_format.text, + "text/css": hug.input_format.text, + "text/html": hug.input_format.text, } directives = { - 'timer': hug.directives.Timer, - 'api': hug.directives.api, - 'module': hug.directives.module, - 'current_api': hug.directives.CurrentAPI, - 'api_version': hug.directives.api_version, - 'user': hug.directives.user, - 'session': hug.directives.session, - 'documentation': hug.directives.documentation + "timer": hug.directives.Timer, + "api": hug.directives.api, + "module": hug.directives.module, + "current_api": hug.directives.CurrentAPI, + "api_version": hug.directives.api_version, + "user": hug.directives.user, + "session": hug.directives.session, + "documentation": hug.directives.documentation, } diff --git a/hug/development_runner.py b/hug/development_runner.py index 58edc2c2..e1ebba70 100644 --- a/hug/development_runner.py +++ b/hug/development_runner.py @@ -43,11 +43,17 @@ def _start_api(api_module, host, port, no_404_documentation, show_intro=True): @cli(version=current) -def hug(file: 'A Python file that contains a Hug API'=None, module: 'A Python module that contains a Hug API'=None, - host: 'Interface to bind to'='', port: number=8000, no_404_documentation: boolean=False, - manual_reload: boolean=False, interval: number=1, - command: 'Run a command defined in the given module'=None, - silent: boolean=False): +def hug( + file: "A Python file that contains a Hug API" = None, + module: "A Python module that contains a Hug API" = None, + host: "Interface to bind to" = "", + port: number = 8000, + no_404_documentation: boolean = False, + manual_reload: boolean = False, + interval: number = 1, + command: "Run a command defined in the given module" = None, + silent: boolean = False, +): """Hug API Development Server""" api_module = None if file and module: @@ -60,7 +66,7 @@ def hug(file: 'A Python file that contains a Hug API'=None, module: 'A Python mo elif module: sys.path.append(os.getcwd()) api_module = importlib.import_module(module) - if not api_module or not hasattr(api_module, '__hug__'): + if not api_module or not hasattr(api_module, "__hug__"): print("Error: must define a file name or module that contains a Hug API.") sys.exit(1) @@ -70,18 +76,22 @@ def hug(file: 'A Python file that contains a Hug API'=None, module: 'A Python mo print(str(api.cli)) sys.exit(1) - sys.argv[1:] = sys.argv[(sys.argv.index('-c') if '-c' in sys.argv else sys.argv.index('--command')) + 2:] + sys.argv[1:] = sys.argv[ + (sys.argv.index("-c") if "-c" in sys.argv else sys.argv.index("--command")) + 2 : + ] api.cli.commands[command]() return ran = False if not manual_reload: - thread.start_new_thread(reload_checker, (interval, )) + thread.start_new_thread(reload_checker, (interval,)) while True: reload_checker.reloading = False time.sleep(1) try: - _start_api(api_module, host, port, no_404_documentation, False if silent else not ran) + _start_api( + api_module, host, port, no_404_documentation, False if silent else not ran + ) except KeyboardInterrupt: if not reload_checker.reloading: sys.exit(1) @@ -89,10 +99,11 @@ def hug(file: 'A Python file that contains a Hug API'=None, module: 'A Python mo ran = True for name in list(sys.modules.keys()): if name not in INIT_MODULES: - del(sys.modules[name]) + del sys.modules[name] if file: - api_module = importlib.machinery.SourceFileLoader(file.split(".")[0], - file).load_module() + api_module = importlib.machinery.SourceFileLoader( + file.split(".")[0], file + ).load_module() elif module: api_module = importlib.import_module(module) else: @@ -104,10 +115,10 @@ def reload_checker(interval): changed = False files = {} for module in list(sys.modules.values()): - path = getattr(module, '__file__', '') + path = getattr(module, "__file__", "") if not path: continue - if path[-4:] in ('.pyo', '.pyc'): + if path[-4:] in (".pyo", ".pyc"): path = path[:-1] if path and exists(path): files[path] = os.stat(path).st_mtime @@ -115,10 +126,10 @@ def reload_checker(interval): while not changed: for path, last_modified in files.items(): if not exists(path): - print('\n> Reloading due to file removal: {}'.format(path)) + print("\n> Reloading due to file removal: {}".format(path)) changed = True elif os.stat(path).st_mtime > last_modified: - print('\n> Reloading due to file change: {}'.format(path)) + print("\n> Reloading due to file change: {}".format(path)) changed = True if changed: diff --git a/hug/directives.py b/hug/directives.py index 63f689f6..423b84e2 100644 --- a/hug/directives.py +++ b/hug/directives.py @@ -39,7 +39,8 @@ def _built_in_directive(directive): @_built_in_directive class Timer(object): """Keeps track of time surpased since instantiation, outputed by doing float(instance)""" - __slots__ = ('start', 'round_to') + + __slots__ = ("start", "round_to") def __init__(self, round_to=None, **kwargs): self.start = python_timer() @@ -89,7 +90,7 @@ def documentation(default=None, api_version=None, api=None, **kwargs): @_built_in_directive -def session(context_name='session', request=None, **kwargs): +def session(context_name="session", request=None, **kwargs): """Returns the session associated with the current request""" return request and request.context.get(context_name, None) @@ -97,20 +98,21 @@ def session(context_name='session', request=None, **kwargs): @_built_in_directive def user(default=None, request=None, **kwargs): """Returns the current logged in user""" - return request and request.context.get('user', None) or default + return request and request.context.get("user", None) or default @_built_in_directive -def cors(support='*', response=None, **kwargs): +def cors(support="*", response=None, **kwargs): """Adds the the Access-Control-Allow-Origin header to this endpoint, with the specified support""" - response and response.set_header('Access-Control-Allow-Origin', support) + response and response.set_header("Access-Control-Allow-Origin", support) return support @_built_in_directive class CurrentAPI(object): """Returns quick access to all api functions on the current version of the api""" - __slots__ = ('api_version', 'api') + + __slots__ = ("api_version", "api") def __init__(self, default=None, api_version=None, **kwargs): self.api_version = api_version @@ -121,12 +123,12 @@ def __getattr__(self, name): if not function: function = self.api.http.versioned.get(None, {}).get(name, None) if not function: - raise AttributeError('API Function {0} not found'.format(name)) + raise AttributeError("API Function {0} not found".format(name)) accepts = function.interface.arguments - if 'hug_api_version' in accepts: + if "hug_api_version" in accepts: function = partial(function, hug_api_version=self.api_version) - if 'hug_current_api' in accepts: + if "hug_current_api" in accepts: function = partial(function, hug_current_api=self) return function diff --git a/hug/exceptions.py b/hug/exceptions.py index fb543d5f..dd54693b 100644 --- a/hug/exceptions.py +++ b/hug/exceptions.py @@ -24,6 +24,7 @@ class InvalidTypeData(Exception): """Should be raised when data passed in doesn't match a types expectations""" + def __init__(self, message, reasons=None): self.message = message self.reasons = reasons @@ -35,4 +36,5 @@ class StoreKeyNotFound(Exception): class SessionNotFound(StoreKeyNotFound): """Should be raised when a session ID has not been found inside a session store""" + pass diff --git a/hug/format.py b/hug/format.py index fba57b0b..f6ecc6d7 100644 --- a/hug/format.py +++ b/hug/format.py @@ -27,29 +27,31 @@ from hug import _empty as empty -UNDERSCORE = (re.compile('(.)([A-Z][a-z]+)'), re.compile('([a-z0-9])([A-Z])')) +UNDERSCORE = (re.compile("(.)([A-Z][a-z]+)"), re.compile("([a-z0-9])([A-Z])")) def parse_content_type(content_type): """Separates out the parameters from the content_type and returns both in a tuple (content_type, parameters)""" - if content_type is not None and ';' in content_type: + if content_type is not None and ";" in content_type: return parse_header(content_type) return (content_type, empty.dict) def content_type(content_type): """Attaches the supplied content_type to a Hug formatting function""" + def decorator(method): method.content_type = content_type return method + return decorator def underscore(text): """Converts text that may be camelcased into an underscored format""" - return UNDERSCORE[1].sub(r'\1_\2', UNDERSCORE[0].sub(r'\1_\2', text)).lower() + return UNDERSCORE[1].sub(r"\1_\2", UNDERSCORE[0].sub(r"\1_\2", text)).lower() def camelcase(text): """Converts text that may be underscored into a camelcase format""" - return text[0] + "".join(text.title().split('_'))[1:] + return text[0] + "".join(text.title().split("_"))[1:] diff --git a/hug/input_format.py b/hug/input_format.py index c8671744..b83d9c1c 100644 --- a/hug/input_format.py +++ b/hug/input_format.py @@ -31,14 +31,14 @@ from hug.json_module import json as json_converter -@content_type('text/plain') -def text(body, charset='utf-8', **kwargs): +@content_type("text/plain") +def text(body, charset="utf-8", **kwargs): """Takes plain text data""" return body.read().decode(charset) -@content_type('application/json') -def json(body, charset='utf-8', **kwargs): +@content_type("application/json") +def json(body, charset="utf-8", **kwargs): """Takes JSON formatted data, converting it into native Python objects""" return json_converter.loads(text(body, charset=charset)) @@ -54,7 +54,7 @@ def _underscore_dict(dictionary): return new_dictionary -def json_underscore(body, charset='utf-8', **kwargs): +def json_underscore(body, charset="utf-8", **kwargs): """Converts JSON formatted date to native Python objects. The keys in any JSON dict are transformed from camelcase to underscore separated words. @@ -62,21 +62,21 @@ def json_underscore(body, charset='utf-8', **kwargs): return _underscore_dict(json(body, charset=charset)) -@content_type('application/x-www-form-urlencoded') -def urlencoded(body, charset='ascii', **kwargs): +@content_type("application/x-www-form-urlencoded") +def urlencoded(body, charset="ascii", **kwargs): """Converts query strings into native Python objects""" return parse_query_string(text(body, charset=charset), False) -@content_type('multipart/form-data') +@content_type("multipart/form-data") def multipart(body, content_length=0, **header_params): """Converts multipart form data into native Python objects""" - header_params.setdefault('CONTENT-LENGTH', content_length) - if header_params and 'boundary' in header_params: - if type(header_params['boundary']) is str: - header_params['boundary'] = header_params['boundary'].encode() + header_params.setdefault("CONTENT-LENGTH", content_length) + if header_params and "boundary" in header_params: + if type(header_params["boundary"]) is str: + header_params["boundary"] = header_params["boundary"].encode() - form = parse_multipart((body.stream if hasattr(body, 'stream') else body), header_params) + form = parse_multipart((body.stream if hasattr(body, "stream") else body), header_params) for key, value in form.items(): if type(value) is list and len(value) is 1: form[key] = value[0] diff --git a/hug/interface.py b/hug/interface.py index cc5c5cb0..01f5156a 100644 --- a/hug/interface.py +++ b/hug/interface.py @@ -38,7 +38,15 @@ from hug import introspect from hug.exceptions import InvalidTypeData from hug.format import parse_content_type -from hug.types import MarshmallowInputSchema, MarshmallowReturnSchema, Multiple, OneOf, SmartBoolean, Text, text +from hug.types import ( + MarshmallowInputSchema, + MarshmallowReturnSchema, + Multiple, + OneOf, + SmartBoolean, + Text, + text, +) def asyncio_call(function, *args, **kwargs): @@ -56,14 +64,14 @@ class Interfaces(object): def __init__(self, function, args=None): self.api = hug.api.from_object(function) - self.spec = getattr(function, 'original', function) + self.spec = getattr(function, "original", function) self.arguments = introspect.arguments(function) self.name = introspect.name(function) self._function = function self.is_coroutine = introspect.is_coroutine(self.spec) if self.is_coroutine: - self.spec = getattr(self.spec, '__wrapped__', self.spec) + self.spec = getattr(self.spec, "__wrapped__", self.spec) self.takes_args = introspect.takes_args(self.spec) self.takes_kwargs = introspect.takes_kwargs(self.spec) @@ -75,7 +83,7 @@ def __init__(self, function, args=None): self.arg = self.parameters.pop(-1) self.parameters = tuple(self.parameters) self.defaults = dict(zip(reversed(self.parameters), reversed(self.spec.__defaults__ or ()))) - self.required = self.parameters[:-(len(self.spec.__defaults__ or ())) or None] + self.required = self.parameters[: -(len(self.spec.__defaults__ or ())) or None] self.is_method = introspect.is_method(self.spec) or introspect.is_method(function) if self.is_method: self.required = self.required[1:] @@ -90,21 +98,21 @@ def __init__(self, function, args=None): else: transformers = self.spec.__annotations__ - self.transform = transformers.get('return', None) + self.transform = transformers.get("return", None) self.directives = {} self.input_transformations = {} for name, transformer in transformers.items(): if isinstance(transformer, str): continue - elif hasattr(transformer, 'directive'): + elif hasattr(transformer, "directive"): self.directives[name] = transformer continue - if hasattr(transformer, 'from_string'): + if hasattr(transformer, "from_string"): transformer = transformer.from_string - elif hasattr(transformer, 'load'): + elif hasattr(transformer, "load"): transformer = MarshmallowInputSchema(transformer) - elif hasattr(transformer, 'deserialize'): + elif hasattr(transformer, "deserialize"): transformer = transformer.deserialize self.input_transformations[name] = transformer @@ -123,89 +131,123 @@ class Interface(object): A Interface object should be created for every kind of protocal hug supports """ - __slots__ = ('interface', '_api', 'defaults', 'parameters', 'required', '_outputs', 'on_invalid', 'requires', - 'validate_function', 'transform', 'examples', 'output_doc', 'wrapped', 'directives', 'all_parameters', - 'raise_on_invalid', 'invalid_outputs', 'map_params', 'input_transformations') + + __slots__ = ( + "interface", + "_api", + "defaults", + "parameters", + "required", + "_outputs", + "on_invalid", + "requires", + "validate_function", + "transform", + "examples", + "output_doc", + "wrapped", + "directives", + "all_parameters", + "raise_on_invalid", + "invalid_outputs", + "map_params", + "input_transformations", + ) def __init__(self, route, function): - if route.get('api', None): - self._api = route['api'] - if 'examples' in route: - self.examples = route['examples'] - function_args = route.get('args') - if not hasattr(function, 'interface'): - function.__dict__['interface'] = Interfaces(function, function_args) + if route.get("api", None): + self._api = route["api"] + if "examples" in route: + self.examples = route["examples"] + function_args = route.get("args") + if not hasattr(function, "interface"): + function.__dict__["interface"] = Interfaces(function, function_args) self.interface = function.interface - self.requires = route.get('requires', ()) - if 'validate' in route: - self.validate_function = route['validate'] - if 'output_invalid' in route: - self.invalid_outputs = route['output_invalid'] + self.requires = route.get("requires", ()) + if "validate" in route: + self.validate_function = route["validate"] + if "output_invalid" in route: + self.invalid_outputs = route["output_invalid"] - if not 'parameters' in route: + if not "parameters" in route: self.defaults = self.interface.defaults self.parameters = self.interface.parameters self.all_parameters = self.interface.all_parameters self.required = self.interface.required else: - self.defaults = route.get('defaults', {}) - self.parameters = tuple(route['parameters']) - self.all_parameters = set(route['parameters']) - self.required = tuple([parameter for parameter in self.parameters if parameter not in self.defaults]) - - if 'map_params' in route: - self.map_params = route['map_params'] + self.defaults = route.get("defaults", {}) + self.parameters = tuple(route["parameters"]) + self.all_parameters = set(route["parameters"]) + self.required = tuple( + [parameter for parameter in self.parameters if parameter not in self.defaults] + ) + + if "map_params" in route: + self.map_params = route["map_params"] for interface_name, internal_name in self.map_params.items(): if internal_name in self.defaults: self.defaults[interface_name] = self.defaults.pop(internal_name) if internal_name in self.parameters: - self.parameters = [interface_name if param == internal_name else param for param in self.parameters] + self.parameters = [ + interface_name if param == internal_name else param + for param in self.parameters + ] if internal_name in self.all_parameters: self.all_parameters.remove(internal_name) self.all_parameters.add(interface_name) if internal_name in self.required: - self.required = tuple([interface_name if param == internal_name else param for - param in self.required]) + self.required = tuple( + [ + interface_name if param == internal_name else param + for param in self.required + ] + ) - reverse_mapping = {internal: interface for interface, internal in self.map_params.items()} - self.input_transformations = {reverse_mapping.get(name, name): transform for - name, transform in self.interface.input_transformations.items()} + reverse_mapping = { + internal: interface for interface, internal in self.map_params.items() + } + self.input_transformations = { + reverse_mapping.get(name, name): transform + for name, transform in self.interface.input_transformations.items() + } else: self.input_transformations = self.interface.input_transformations - if 'output' in route: - self.outputs = route['output'] + if "output" in route: + self.outputs = route["output"] - self.transform = route.get('transform', None) + self.transform = route.get("transform", None) if self.transform is None and not isinstance(self.interface.transform, (str, type(None))): self.transform = self.interface.transform - if hasattr(self.transform, 'dump'): + if hasattr(self.transform, "dump"): self.transform = MarshmallowReturnSchema(self.transform) self.output_doc = self.transform.__doc__ elif self.transform or self.interface.transform: - output_doc = (self.transform or self.interface.transform) + output_doc = self.transform or self.interface.transform self.output_doc = output_doc if type(output_doc) is str else output_doc.__doc__ - self.raise_on_invalid = route.get('raise_on_invalid', False) - if 'on_invalid' in route: - self.on_invalid = route['on_invalid'] + self.raise_on_invalid = route.get("raise_on_invalid", False) + if "on_invalid" in route: + self.on_invalid = route["on_invalid"] elif self.transform: self.on_invalid = self.transform defined_directives = self.api.directives() used_directives = set(self.parameters).intersection(defined_directives) - self.directives = {directive_name: defined_directives[directive_name] for directive_name in used_directives} + self.directives = { + directive_name: defined_directives[directive_name] for directive_name in used_directives + } self.directives.update(self.interface.directives) @property def api(self): - return getattr(self, '_api', self.interface.api) + return getattr(self, "_api", self.interface.api) @property def outputs(self): - return getattr(self, '_outputs', None) + return getattr(self, "_outputs", None) @outputs.setter def outputs(self, outputs): @@ -219,29 +261,25 @@ def validate(self, input_parameters, context): if self.raise_on_invalid: if key in input_parameters: input_parameters[key] = self.initialize_handler( - type_handler, - input_parameters[key], - context=context + type_handler, input_parameters[key], context=context ) else: try: if key in input_parameters: input_parameters[key] = self.initialize_handler( - type_handler, - input_parameters[key], - context=context + type_handler, input_parameters[key], context=context ) except InvalidTypeData as error: errors[key] = error.reasons or str(error.message) except Exception as error: - if hasattr(error, 'args') and error.args: + if hasattr(error, "args") and error.args: errors[key] = error.args[0] else: errors[key] = str(error) for require in self.required: if not require in input_parameters: errors[require] = "Required parameter '{}' not supplied".format(require) - if not errors and getattr(self, 'validate_function', False): + if not errors and getattr(self, "validate_function", False): errors = self.validate_function(input_parameters) return errors @@ -252,7 +290,9 @@ def check_requirements(self, request=None, response=None, context=None): otherwise, the error reported will be returned """ for requirement in self.requires: - conclusion = requirement(response=response, request=request, context=context, module=self.api.module) + conclusion = requirement( + response=response, request=request, context=context, module=self.api.module + ) if conclusion and conclusion is not True: return conclusion @@ -262,29 +302,36 @@ def documentation(self, add_to=None): usage = self.interface.spec.__doc__ if usage: - doc['usage'] = usage - if getattr(self, 'requires', None): - doc['requires'] = [getattr(requirement, '__doc__', requirement.__name__) for requirement in self.requires] - doc['outputs'] = OrderedDict() - doc['outputs']['format'] = self.outputs.__doc__ - doc['outputs']['content_type'] = self.outputs.content_type - parameters = [param for param in self.parameters if not param in ('request', 'response', 'self') - and not param in ('api_version', 'body') - and not param.startswith('hug_') - and not hasattr(param, 'directive')] + doc["usage"] = usage + if getattr(self, "requires", None): + doc["requires"] = [ + getattr(requirement, "__doc__", requirement.__name__) + for requirement in self.requires + ] + doc["outputs"] = OrderedDict() + doc["outputs"]["format"] = self.outputs.__doc__ + doc["outputs"]["content_type"] = self.outputs.content_type + parameters = [ + param + for param in self.parameters + if not param in ("request", "response", "self") + and not param in ("api_version", "body") + and not param.startswith("hug_") + and not hasattr(param, "directive") + ] if parameters: - inputs = doc.setdefault('inputs', OrderedDict()) + inputs = doc.setdefault("inputs", OrderedDict()) types = self.interface.spec.__annotations__ for argument in parameters: kind = types.get(argument, text) - if getattr(kind, 'directive', None) is True: + if getattr(kind, "directive", None) is True: continue input_definition = inputs.setdefault(argument, OrderedDict()) - input_definition['type'] = kind if isinstance(kind, str) else kind.__doc__ + input_definition["type"] = kind if isinstance(kind, str) else kind.__doc__ default = self.defaults.get(argument, None) if default is not None: - input_definition['default'] = default + input_definition["default"] = default return doc @@ -296,7 +343,7 @@ def _rewrite_params(self, params): @staticmethod def cleanup_parameters(parameters, exception=None): for parameter, directive in parameters.items(): - if hasattr(directive, 'cleanup'): + if hasattr(directive, "cleanup"): directive.cleanup(exception=exception) @staticmethod @@ -309,14 +356,15 @@ def initialize_handler(handler, value, context): class Local(Interface): """Defines the Interface responsible for exposing functions locally""" - __slots__ = ('skip_directives', 'skip_validation', 'version') + + __slots__ = ("skip_directives", "skip_validation", "version") def __init__(self, route, function): super().__init__(route, function) - self.version = route.get('version', None) - if 'skip_directives' in route: + self.version = route.get("version", None) + if "skip_directives" in route: self.skip_directives = True - if 'skip_validation' in route: + if "skip_validation" in route: self.skip_validation = True self.interface.local = self @@ -346,30 +394,35 @@ def __call__(self, *args, **kwargs): for index, argument in enumerate(args): kwargs[self.parameters[index]] = argument - if not getattr(self, 'skip_directives', False): + if not getattr(self, "skip_directives", False): for parameter, directive in self.directives.items(): if parameter in kwargs: continue - arguments = (self.defaults[parameter], ) if parameter in self.defaults else () - kwargs[parameter] = directive(*arguments, api=self.api, api_version=self.version, - interface=self, context=context) + arguments = (self.defaults[parameter],) if parameter in self.defaults else () + kwargs[parameter] = directive( + *arguments, + api=self.api, + api_version=self.version, + interface=self, + context=context + ) - if not getattr(self, 'skip_validation', False): + if not getattr(self, "skip_validation", False): errors = self.validate(kwargs, context) if errors: - errors = {'errors': errors} - if getattr(self, 'on_invalid', False): + errors = {"errors": errors} + if getattr(self, "on_invalid", False): errors = self.on_invalid(errors) - outputs = getattr(self, 'invalid_outputs', self.outputs) + outputs = getattr(self, "invalid_outputs", self.outputs) self.api.delete_context(context, errors=errors) return outputs(errors) if outputs else errors - if getattr(self, 'map_params', None): + if getattr(self, "map_params", None): self._rewrite_params(kwargs) try: result = self.interface(**kwargs) if self.transform: - if hasattr(self.transform, 'context'): + if hasattr(self.transform, "context"): self.transform.context = context result = self.transform(result) except Exception as exception: @@ -392,11 +445,13 @@ def __init__(self, route, function): self.interface.cli = self self.reaffirm_types = {} use_parameters = list(self.interface.parameters) - self.additional_options = getattr(self.interface, 'arg', getattr(self.interface, 'kwarg', False)) + self.additional_options = getattr( + self.interface, "arg", getattr(self.interface, "kwarg", False) + ) if self.additional_options: use_parameters.append(self.additional_options) - used_options = {'h', 'help'} + used_options = {"h", "help"} nargs_set = self.interface.takes_args or self.interface.takes_kwargs class CustomArgumentParser(argparse.ArgumentParser): @@ -407,12 +462,19 @@ def exit(self, status=0, message=None): self.exit_callback(message) super().exit(status, message) - self.parser = CustomArgumentParser(description=route.get('doc', self.interface.spec.__doc__)) - if 'version' in route: - self.parser.add_argument('-v', '--version', action='version', - version="{0} {1}".format(route.get('name', self.interface.spec.__name__), - route['version'])) - used_options.update(('v', 'version')) + self.parser = CustomArgumentParser( + description=route.get("doc", self.interface.spec.__doc__) + ) + if "version" in route: + self.parser.add_argument( + "-v", + "--version", + action="version", + version="{0} {1}".format( + route.get("name", self.interface.spec.__name__), route["version"] + ), + ) + used_options.update(("v", "version")) self.context_tranforms = [] for option in use_parameters: @@ -421,70 +483,77 @@ def exit(self, status=0, message=None): continue if option in self.interface.required or option == self.additional_options: - args = (option, ) + args = (option,) else: short_option = option[0] while short_option in used_options and len(short_option) < len(option): - short_option = option[:len(short_option) + 1] + short_option = option[: len(short_option) + 1] used_options.add(short_option) used_options.add(option) if short_option != option: - args = ('-{0}'.format(short_option), '--{0}'.format(option)) + args = ("-{0}".format(short_option), "--{0}".format(option)) else: - args = ('--{0}'.format(option), ) + args = ("--{0}".format(option),) kwargs = {} if option in self.defaults: - kwargs['default'] = self.defaults[option] + kwargs["default"] = self.defaults[option] if option in self.interface.input_transformations: transform = self.interface.input_transformations[option] - kwargs['type'] = transform - kwargs['help'] = transform.__doc__ + kwargs["type"] = transform + kwargs["help"] = transform.__doc__ if transform in (list, tuple) or isinstance(transform, types.Multiple): - kwargs['action'] = 'append' - kwargs['type'] = Text() + kwargs["action"] = "append" + kwargs["type"] = Text() self.reaffirm_types[option] = transform elif transform == bool or isinstance(transform, type(types.boolean)): - kwargs['action'] = 'store_true' + kwargs["action"] = "store_true" self.reaffirm_types[option] = transform elif isinstance(transform, types.OneOf): - kwargs['choices'] = transform.values - elif (option in self.interface.spec.__annotations__ and - type(self.interface.spec.__annotations__[option]) == str): - kwargs['help'] = option - if ((kwargs.get('type', None) == bool or kwargs.get('action', None) == 'store_true') and - not kwargs['default']): - kwargs['action'] = 'store_true' - kwargs.pop('type', None) - elif kwargs.get('action', None) == 'store_true': - kwargs.pop('action', None) == 'store_true' + kwargs["choices"] = transform.values + elif ( + option in self.interface.spec.__annotations__ + and type(self.interface.spec.__annotations__[option]) == str + ): + kwargs["help"] = option + if ( + kwargs.get("type", None) == bool or kwargs.get("action", None) == "store_true" + ) and not kwargs["default"]: + kwargs["action"] = "store_true" + kwargs.pop("type", None) + elif kwargs.get("action", None) == "store_true": + kwargs.pop("action", None) == "store_true" if option == self.additional_options: - kwargs['nargs'] = '*' - elif not nargs_set and kwargs.get('action', None) == 'append' and not option in self.interface.defaults: - kwargs['nargs'] = '*' - kwargs.pop('action', '') + kwargs["nargs"] = "*" + elif ( + not nargs_set + and kwargs.get("action", None) == "append" + and not option in self.interface.defaults + ): + kwargs["nargs"] = "*" + kwargs.pop("action", "") nargs_set = True self.parser.add_argument(*args, **kwargs) - self.api.cli.commands[route.get('name', self.interface.spec.__name__)] = self + self.api.cli.commands[route.get("name", self.interface.spec.__name__)] = self def output(self, data, context): """Outputs the provided data using the transformations and output format specified for this CLI endpoint""" if self.transform: - if hasattr(self.transform, 'context'): + if hasattr(self.transform, "context"): self.transform.context = context data = self.transform(data) - if hasattr(data, 'read'): - data = data.read().decode('utf8') + if hasattr(data, "read"): + data = data.read().decode("utf8") if data is not None: data = self.outputs(data) if data: sys.stdout.buffer.write(data) - if not data.endswith(b'\n'): - sys.stdout.buffer.write(b'\n') + if not data.endswith(b"\n"): + sys.stdout.buffer.write(b"\n") return data def __call__(self): @@ -493,6 +562,7 @@ def __call__(self): def exit_callback(message): self.api.delete_context(context, errors=message) + self.parser.exit_callback = exit_callback self.api._ensure_started() @@ -508,19 +578,18 @@ def exit_callback(message): known, unknown = self.parser.parse_known_args() pass_to_function = vars(known) for option, directive in self.directives.items(): - arguments = (self.defaults[option], ) if option in self.defaults else () - pass_to_function[option] = directive(*arguments, api=self.api, argparse=self.parser, context=context, - interface=self) + arguments = (self.defaults[option],) if option in self.defaults else () + pass_to_function[option] = directive( + *arguments, api=self.api, argparse=self.parser, context=context, interface=self + ) for field, type_handler in self.reaffirm_types.items(): if field in pass_to_function: pass_to_function[field] = self.initialize_handler( - type_handler, - pass_to_function[field], - context=context + type_handler, pass_to_function[field], context=context ) - if getattr(self, 'validate_function', False): + if getattr(self, "validate_function", False): errors = self.validate_function(pass_to_function) if errors: self.api.delete_context(context, errors=errors) @@ -536,7 +605,7 @@ def exit_callback(message): if self.interface.takes_kwargs: add_options_to = None for index, option in enumerate(unknown): - if option.startswith('--'): + if option.startswith("--"): if add_options_to: value = pass_to_function[add_options_to] if len(value) == 1: @@ -548,7 +617,7 @@ def exit_callback(message): elif add_options_to: pass_to_function[add_options_to].append(option) - if getattr(self, 'map_params', None): + if getattr(self, "map_params", None): self._rewrite_params(pass_to_function) try: @@ -567,46 +636,68 @@ def exit_callback(message): class HTTP(Interface): """Defines the interface responsible for wrapping functions and exposing them via HTTP based on the route""" - __slots__ = ('_params_for_outputs_state', '_params_for_invalid_outputs_state', '_params_for_transform_state', - '_params_for_on_invalid', 'set_status', 'response_headers', 'transform', 'input_transformations', - 'examples', 'wrapped', 'catch_exceptions', 'parse_body', 'private', 'on_invalid', 'inputs') - AUTO_INCLUDE = {'request', 'response'} + + __slots__ = ( + "_params_for_outputs_state", + "_params_for_invalid_outputs_state", + "_params_for_transform_state", + "_params_for_on_invalid", + "set_status", + "response_headers", + "transform", + "input_transformations", + "examples", + "wrapped", + "catch_exceptions", + "parse_body", + "private", + "on_invalid", + "inputs", + ) + AUTO_INCLUDE = {"request", "response"} def __init__(self, route, function, catch_exceptions=True): super().__init__(route, function) self.catch_exceptions = catch_exceptions - self.parse_body = 'parse_body' in route - self.set_status = route.get('status', False) - self.response_headers = tuple(route.get('response_headers', {}).items()) - self.private = 'private' in route - self.inputs = route.get('inputs', {}) - - if 'on_invalid' in route: - self._params_for_on_invalid = introspect.takes_arguments(self.on_invalid, *self.AUTO_INCLUDE) + self.parse_body = "parse_body" in route + self.set_status = route.get("status", False) + self.response_headers = tuple(route.get("response_headers", {}).items()) + self.private = "private" in route + self.inputs = route.get("inputs", {}) + + if "on_invalid" in route: + self._params_for_on_invalid = introspect.takes_arguments( + self.on_invalid, *self.AUTO_INCLUDE + ) elif self.transform: self._params_for_on_invalid = self._params_for_transform - self.api.http.versions.update(route.get('versions', (None, ))) + self.api.http.versions.update(route.get("versions", (None,))) self.interface.http = self @property def _params_for_outputs(self): - if not hasattr(self, '_params_for_outputs_state'): - self._params_for_outputs_state = introspect.takes_arguments(self.outputs, *self.AUTO_INCLUDE) + if not hasattr(self, "_params_for_outputs_state"): + self._params_for_outputs_state = introspect.takes_arguments( + self.outputs, *self.AUTO_INCLUDE + ) return self._params_for_outputs_state @property def _params_for_invalid_outputs(self): - if not hasattr(self, '_params_for_invalid_outputs_state'): - self._params_for_invalid_outputs_state = introspect.takes_arguments(self.invalid_outputs, - *self.AUTO_INCLUDE) + if not hasattr(self, "_params_for_invalid_outputs_state"): + self._params_for_invalid_outputs_state = introspect.takes_arguments( + self.invalid_outputs, *self.AUTO_INCLUDE + ) return self._params_for_invalid_outputs_state @property def _params_for_transform(self): - if not hasattr(self, '_params_for_transform_state'): - self._params_for_transform_state = introspect.takes_arguments(self.transform, *self.AUTO_INCLUDE) + if not hasattr(self, "_params_for_transform_state"): + self._params_for_transform_state = introspect.takes_arguments( + self.transform, *self.AUTO_INCLUDE + ) return self._params_for_transform_state def gather_parameters(self, request, response, context, api_version=None, **input_parameters): @@ -616,32 +707,40 @@ def gather_parameters(self, request, response, context, api_version=None, **inpu if self.parse_body and request.content_length: body = request.stream content_type, content_params = parse_content_type(request.content_type) - body_formatter = body and self.inputs.get(content_type, self.api.http.input_format(content_type)) + body_formatter = body and self.inputs.get( + content_type, self.api.http.input_format(content_type) + ) if body_formatter: body = body_formatter(body, content_length=request.content_length, **content_params) - if 'body' in self.all_parameters: - input_parameters['body'] = body + if "body" in self.all_parameters: + input_parameters["body"] = body if isinstance(body, dict): input_parameters.update(body) - elif 'body' in self.all_parameters: - input_parameters['body'] = None - - if 'request' in self.all_parameters: - input_parameters['request'] = request - if 'response' in self.all_parameters: - input_parameters['response'] = response - if 'api_version' in self.all_parameters: - input_parameters['api_version'] = api_version + elif "body" in self.all_parameters: + input_parameters["body"] = None + + if "request" in self.all_parameters: + input_parameters["request"] = request + if "response" in self.all_parameters: + input_parameters["response"] = response + if "api_version" in self.all_parameters: + input_parameters["api_version"] = api_version for parameter, directive in self.directives.items(): - arguments = (self.defaults[parameter], ) if parameter in self.defaults else () - input_parameters[parameter] = directive(*arguments, response=response, request=request, - api=self.api, api_version=api_version, context=context, - interface=self) + arguments = (self.defaults[parameter],) if parameter in self.defaults else () + input_parameters[parameter] = directive( + *arguments, + response=response, + request=request, + api=self.api, + api_version=api_version, + context=context, + interface=self + ) return input_parameters @property def outputs(self): - return getattr(self, '_outputs', self.api.http.output_format) + return getattr(self, "_outputs", self.api.http.output_format) @outputs.setter def outputs(self, outputs): @@ -649,12 +748,14 @@ def outputs(self, outputs): def transform_data(self, data, request=None, response=None, context=None): transform = self.transform - if hasattr(transform, 'context'): + if hasattr(transform, "context"): self.transform.context = context """Runs the transforms specified on this endpoint with the provided data, returning the data modified""" if transform and not (isinstance(transform, type) and isinstance(data, transform)): if self._params_for_transform: - return transform(data, **self._arguments(self._params_for_transform, request, response)) + return transform( + data, **self._arguments(self._params_for_transform, request, response) + ) else: return transform(data) return data @@ -676,10 +777,10 @@ def invalid_content_type(self, request=None, response=None): def _arguments(self, requested_params, request=None, response=None): if requested_params: arguments = {} - if 'response' in requested_params: - arguments['response'] = response - if 'request' in requested_params: - arguments['request'] = request + if "response" in requested_params: + arguments["response"] = response + if "request" in requested_params: + arguments["request"] = request return arguments return empty.dict @@ -693,28 +794,37 @@ def set_response_defaults(self, response, request=None): response.content_type = self.content_type(request, response) def render_errors(self, errors, request, response): - data = {'errors': errors} - if getattr(self, 'on_invalid', False): - data = self.on_invalid(data, **self._arguments(self._params_for_on_invalid, request, response)) + data = {"errors": errors} + if getattr(self, "on_invalid", False): + data = self.on_invalid( + data, **self._arguments(self._params_for_on_invalid, request, response) + ) response.status = HTTP_BAD_REQUEST - if getattr(self, 'invalid_outputs', False): + if getattr(self, "invalid_outputs", False): response.content_type = self.invalid_content_type(request, response) - response.data = self.invalid_outputs(data, **self._arguments(self._params_for_invalid_outputs, - request, response)) + response.data = self.invalid_outputs( + data, **self._arguments(self._params_for_invalid_outputs, request, response) + ) else: - response.data = self.outputs(data, **self._arguments(self._params_for_outputs, request, response)) + response.data = self.outputs( + data, **self._arguments(self._params_for_outputs, request, response) + ) def call_function(self, parameters): if not self.interface.takes_kwargs: - parameters = {key: value for key, value in parameters.items() if key in self.all_parameters} - if getattr(self, 'map_params', None): + parameters = { + key: value for key, value in parameters.items() if key in self.all_parameters + } + if getattr(self, "map_params", None): self._rewrite_params(parameters) return self.interface(**parameters) def render_content(self, content, context, request, response, **kwargs): - if hasattr(content, 'interface') and (content.interface is True or hasattr(content.interface, 'http')): + if hasattr(content, "interface") and ( + content.interface is True or hasattr(content.interface, "http") + ): if content.interface is True: content(request, response, api_version=None, **kwargs) else: @@ -722,10 +832,12 @@ def render_content(self, content, context, request, response, **kwargs): return content = self.transform_data(content, request, response, context) - content = self.outputs(content, **self._arguments(self._params_for_outputs, request, response)) - if hasattr(content, 'read'): + content = self.outputs( + content, **self._arguments(self._params_for_outputs, request, response) + ) + if hasattr(content, "read"): size = None - if hasattr(content, 'name') and os.path.isfile(content.name): + if hasattr(content, "name") and os.path.isfile(content.name): size = os.path.getsize(content.name) if request.range and size: start, end = request.range @@ -747,8 +859,13 @@ def render_content(self, content, context, request, response, **kwargs): response.data = content def __call__(self, request, response, api_version=None, **kwargs): - context = self.api.context_factory(response=response, request=request, api=self.api, api_version=api_version, - interface=self) + context = self.api.context_factory( + response=response, + request=request, + api=self.api, + api_version=api_version, + interface=self, + ) """Call the wrapped function over HTTP pulling information as needed""" if isinstance(api_version, str) and api_version.isdigit(): api_version = int(api_version) @@ -764,18 +881,24 @@ def __call__(self, request, response, api_version=None, **kwargs): self.set_response_defaults(response, request) lacks_requirement = self.check_requirements(request, response, context) if lacks_requirement: - response.data = self.outputs(lacks_requirement, - **self._arguments(self._params_for_outputs, request, response)) + response.data = self.outputs( + lacks_requirement, + **self._arguments(self._params_for_outputs, request, response) + ) self.api.delete_context(context, lacks_requirement=lacks_requirement) return - input_parameters = self.gather_parameters(request, response, context, api_version, **kwargs) + input_parameters = self.gather_parameters( + request, response, context, api_version, **kwargs + ) errors = self.validate(input_parameters, context) if errors: self.api.delete_context(context, errors=errors) return self.render_errors(errors, request, response) - self.render_content(self.call_function(input_parameters), context, request, response, **kwargs) + self.render_content( + self.call_function(input_parameters), context, request, response, **kwargs + ) except falcon.HTTPNotFound as exception: self.cleanup_parameters(input_parameters, exception=exception) self.api.delete_context(context, exception=exception) @@ -788,11 +911,12 @@ def __call__(self, request, response, api_version=None, **kwargs): if exception_type in exception_types: handler = self.api.http.exception_handlers(api_version)[exception_type][0] else: - for match_exception_type, exception_handlers in \ - tuple(self.api.http.exception_handlers(api_version).items())[::-1]: + for match_exception_type, exception_handlers in tuple( + self.api.http.exception_handlers(api_version).items() + )[::-1]: if isinstance(exception, match_exception_type): for potential_handler in exception_handlers: - if not isinstance(exception, potential_handler.exclude): + if not isinstance(exception, potential_handler.exclude): handler = potential_handler if not handler: @@ -812,20 +936,22 @@ def documentation(self, add_to=None, version=None, prefix="", base_url="", url=" usage = self.interface.spec.__doc__ if usage: - doc['usage'] = usage + doc["usage"] = usage for example in self.examples: - example_text = "{0}{1}{2}{3}".format(prefix, base_url, '/v{0}'.format(version) if version else '', url) + example_text = "{0}{1}{2}{3}".format( + prefix, base_url, "/v{0}".format(version) if version else "", url + ) if isinstance(example, str): example_text += "?{0}".format(example) - doc_examples = doc.setdefault('examples', []) + doc_examples = doc.setdefault("examples", []) if not example_text in doc_examples: doc_examples.append(example_text) doc = super().documentation(doc) - if getattr(self, 'output_doc', ''): - doc['outputs']['type'] = self.output_doc + if getattr(self, "output_doc", ""): + doc["outputs"]["type"] = self.output_doc return doc @@ -839,25 +965,26 @@ def urls(self, version=None): for interface_version, interface in versions.items(): if interface_version == version and interface == self: if not url in urls: - urls.append(('/v{0}'.format(version) if version else '') + url) + urls.append(("/v{0}".format(version) if version else "") + url) return urls def url(self, version=None, **kwargs): """Returns the first matching URL found for the specified arguments""" for url in self.urls(version): - if [key for key in kwargs.keys() if not '{' + key + '}' in url]: + if [key for key in kwargs.keys() if not "{" + key + "}" in url]: continue return url.format(**kwargs) - raise KeyError('URL that takes all provided parameters not found') + raise KeyError("URL that takes all provided parameters not found") class ExceptionRaised(HTTP): """Defines the interface responsible for taking and transforming exceptions that occur during processing""" - __slots__ = ('handle', 'exclude') + + __slots__ = ("handle", "exclude") def __init__(self, route, *args, **kwargs): - self.handle = route['exceptions'] - self.exclude = route['exclude'] + self.handle = route["exceptions"] + self.exclude = route["exclude"] super().__init__(route, *args, **kwargs) diff --git a/hug/introspect.py b/hug/introspect.py index eda31357..9cddfd6c 100644 --- a/hug/introspect.py +++ b/hug/introspect.py @@ -32,7 +32,7 @@ def is_method(function): def is_coroutine(function): """Returns True if the passed in function is a coroutine""" - return function.__code__.co_flags & 0x0080 or getattr(function, '_is_coroutine', False) + return function.__code__.co_flags & 0x0080 or getattr(function, "_is_coroutine", False) def name(function): @@ -42,10 +42,10 @@ def name(function): def arguments(function, extra_arguments=0): """Returns the name of all arguments a function takes""" - if not hasattr(function, '__code__'): + if not hasattr(function, "__code__"): return () - return function.__code__.co_varnames[:function.__code__.co_argcount + extra_arguments] + return function.__code__.co_varnames[: function.__code__.co_argcount + extra_arguments] def takes_kwargs(function): @@ -72,7 +72,7 @@ def generate_accepted_kwargs(function, *named_arguments): """Dynamically creates a function that when called with dictionary of arguments will produce a kwarg that's compatible with the supplied function """ - if hasattr(function, '__code__') and takes_kwargs(function): + if hasattr(function, "__code__") and takes_kwargs(function): function_takes_kwargs = True function_takes_arguments = [] else: @@ -85,4 +85,5 @@ def accepted_kwargs(kwargs): elif function_takes_arguments: return {key: value for key, value in kwargs.items() if key in function_takes_arguments} return {} + return accepted_kwargs diff --git a/hug/json_module.py b/hug/json_module.py index f0fb47e4..5df4f4ae 100644 --- a/hug/json_module.py +++ b/hug/json_module.py @@ -1,6 +1,6 @@ import os -HUG_USE_UJSON = bool(os.environ.get('HUG_USE_UJSON', 1)) +HUG_USE_UJSON = bool(os.environ.get("HUG_USE_UJSON", 1)) try: # pragma: no cover if HUG_USE_UJSON: import ujson as json @@ -9,11 +9,12 @@ class dumps_proxy: """Proxies the call so non supported kwargs are skipped and it enables escape_forward_slashes to simulate built-in json """ + _dumps = json.dumps def __call__(self, *args, **kwargs): - kwargs.pop('default', None) - kwargs.pop('separators', None) + kwargs.pop("default", None) + kwargs.pop("separators", None) kwargs.update(escape_forward_slashes=False) try: return self._dumps(*args, **kwargs) diff --git a/hug/middleware.py b/hug/middleware.py index 856cfb9d..61bd0ffb 100644 --- a/hug/middleware.py +++ b/hug/middleware.py @@ -39,11 +39,31 @@ class SessionMiddleware(object): The name of the context key can be set via the 'context_name' argument. The cookie arguments are the same as for falcons set_cookie() function, just prefixed with 'cookie_'. """ - __slots__ = ('store', 'context_name', 'cookie_name', 'cookie_expires', 'cookie_max_age', 'cookie_domain', - 'cookie_path', 'cookie_secure', 'cookie_http_only') - def __init__(self, store, context_name='session', cookie_name='sid', cookie_expires=None, cookie_max_age=None, - cookie_domain=None, cookie_path=None, cookie_secure=True, cookie_http_only=True): + __slots__ = ( + "store", + "context_name", + "cookie_name", + "cookie_expires", + "cookie_max_age", + "cookie_domain", + "cookie_path", + "cookie_secure", + "cookie_http_only", + ) + + def __init__( + self, + store, + context_name="session", + cookie_name="sid", + cookie_expires=None, + cookie_max_age=None, + cookie_domain=None, + cookie_path=None, + cookie_secure=True, + cookie_http_only=True, + ): self.store = store self.context_name = context_name self.cookie_name = cookie_name @@ -76,29 +96,47 @@ def process_response(self, request, response, resource, req_succeeded): sid = self.generate_sid() self.store.set(sid, request.context.get(self.context_name, {})) - response.set_cookie(self.cookie_name, sid, expires=self.cookie_expires, max_age=self.cookie_max_age, - domain=self.cookie_domain, path=self.cookie_path, secure=self.cookie_secure, - http_only=self.cookie_http_only) + response.set_cookie( + self.cookie_name, + sid, + expires=self.cookie_expires, + max_age=self.cookie_max_age, + domain=self.cookie_domain, + path=self.cookie_path, + secure=self.cookie_secure, + http_only=self.cookie_http_only, + ) class LogMiddleware(object): """A middleware that logs all incoming requests and outgoing responses that make their way through the API""" - __slots__ = ('logger', ) + + __slots__ = ("logger",) def __init__(self, logger=None): - self.logger = logger if logger is not None else logging.getLogger('hug') + self.logger = logger if logger is not None else logging.getLogger("hug") def _generate_combined_log(self, request, response): """Given a request/response pair, generate a logging format similar to the NGINX combined style.""" current_time = datetime.utcnow() - data_len = '-' if response.data is None else len(response.data) - return '{0} - - [{1}] {2} {3} {4} {5} {6}'.format(request.remote_addr, current_time, request.method, - request.relative_uri, response.status, - data_len, request.user_agent) + data_len = "-" if response.data is None else len(response.data) + return "{0} - - [{1}] {2} {3} {4} {5} {6}".format( + request.remote_addr, + current_time, + request.method, + request.relative_uri, + response.status, + data_len, + request.user_agent, + ) def process_request(self, request, response): """Logs the basic endpoint requested""" - self.logger.info('Requested: {0} {1} {2}'.format(request.method, request.relative_uri, request.content_type)) + self.logger.info( + "Requested: {0} {1} {2}".format( + request.method, request.relative_uri, request.content_type + ) + ) def process_response(self, request, response, resource, req_succeeded): """Logs the basic data returned by the API""" @@ -111,9 +149,12 @@ class CORSMiddleware(object): Adds appropriate Access-Control-* headers to the HTTP responses returned from the hug API, especially for HTTP OPTIONS responses used in CORS preflighting. """ - __slots__ = ('api', 'allow_origins', 'allow_credentials', 'max_age') - def __init__(self, api, allow_origins: list=['*'], allow_credentials: bool=True, max_age: int=None): + __slots__ = ("api", "allow_origins", "allow_credentials", "max_age") + + def __init__( + self, api, allow_origins: list = ["*"], allow_credentials: bool = True, max_age: int = None + ): self.api = api self.allow_origins = allow_origins self.allow_credentials = allow_credentials @@ -125,38 +166,38 @@ def match_route(self, reqpath): routes = [route for route, _ in route_dicts.items()] if reqpath not in routes: for route in routes: # replace params in route with regex - reqpath = re.sub('^(/v\d*/?)', '/', reqpath) - base_url = getattr(self.api.http, 'base_url', '') - reqpath = reqpath.replace(base_url, '', 1) if base_url else reqpath - if re.match(re.sub(r'/{[^{}]+}', r'/[\\w-]+', route) + '$', reqpath): + reqpath = re.sub("^(/v\d*/?)", "/", reqpath) + base_url = getattr(self.api.http, "base_url", "") + reqpath = reqpath.replace(base_url, "", 1) if base_url else reqpath + if re.match(re.sub(r"/{[^{}]+}", r"/[\\w-]+", route) + "$", reqpath): return route return reqpath def process_response(self, request, response, resource, req_succeeded): """Add CORS headers to the response""" - response.set_header('Access-Control-Allow-Credentials', str(self.allow_credentials).lower()) + response.set_header("Access-Control-Allow-Credentials", str(self.allow_credentials).lower()) - origin = request.get_header('ORIGIN') - if origin and (origin in self.allow_origins) or ('*' in self.allow_origins): - response.set_header('Access-Control-Allow-Origin', origin) + origin = request.get_header("ORIGIN") + if origin and (origin in self.allow_origins) or ("*" in self.allow_origins): + response.set_header("Access-Control-Allow-Origin", origin) - if request.method == 'OPTIONS': # check if we are handling a preflight request + if request.method == "OPTIONS": # check if we are handling a preflight request allowed_methods = set( method for _, routes in self.api.http.routes.items() for method, _ in routes[self.match_route(request.path)].items() ) - allowed_methods.add('OPTIONS') + allowed_methods.add("OPTIONS") # return allowed methods - response.set_header('Access-Control-Allow-Methods', ', '.join(allowed_methods)) - response.set_header('Allow', ', '.join(allowed_methods)) + response.set_header("Access-Control-Allow-Methods", ", ".join(allowed_methods)) + response.set_header("Allow", ", ".join(allowed_methods)) # get all requested headers and echo them back - requested_headers = request.get_header('Access-Control-Request-Headers') - response.set_header('Access-Control-Allow-Headers', requested_headers or '') + requested_headers = request.get_header("Access-Control-Request-Headers") + response.set_header("Access-Control-Allow-Headers", requested_headers or "") # return valid caching time if self.max_age: - response.set_header('Access-Control-Max-Age', self.max_age) + response.set_header("Access-Control-Max-Age", self.max_age) diff --git a/hug/output_format.py b/hug/output_format.py index a069b597..1d50d38c 100644 --- a/hug/output_format.py +++ b/hug/output_format.py @@ -45,19 +45,61 @@ except ImportError: numpy = False -IMAGE_TYPES = ('png', 'jpg', 'bmp', 'eps', 'gif', 'im', 'jpeg', 'msp', 'pcx', 'ppm', 'spider', 'tiff', 'webp', 'xbm', - 'cur', 'dcx', 'fli', 'flc', 'gbr', 'gd', 'ico', 'icns', 'imt', 'iptc', 'naa', 'mcidas', 'mpo', 'pcd', - 'psd', 'sgi', 'tga', 'wal', 'xpm', 'svg', 'svg+xml') - -VIDEO_TYPES = (('flv', 'video/x-flv'), ('mp4', 'video/mp4'), ('m3u8', 'application/x-mpegURL'), ('ts', 'video/MP2T'), - ('3gp', 'video/3gpp'), ('mov', 'video/quicktime'), ('avi', 'video/x-msvideo'), ('wmv', 'video/x-ms-wmv')) +IMAGE_TYPES = ( + "png", + "jpg", + "bmp", + "eps", + "gif", + "im", + "jpeg", + "msp", + "pcx", + "ppm", + "spider", + "tiff", + "webp", + "xbm", + "cur", + "dcx", + "fli", + "flc", + "gbr", + "gd", + "ico", + "icns", + "imt", + "iptc", + "naa", + "mcidas", + "mpo", + "pcd", + "psd", + "sgi", + "tga", + "wal", + "xpm", + "svg", + "svg+xml", +) + +VIDEO_TYPES = ( + ("flv", "video/x-flv"), + ("mp4", "video/mp4"), + ("m3u8", "application/x-mpegURL"), + ("ts", "video/MP2T"), + ("3gp", "video/3gpp"), + ("mov", "video/quicktime"), + ("avi", "video/x-msvideo"), + ("wmv", "video/x-ms-wmv"), +) RE_ACCEPT_QUALITY = re.compile("q=(?P[^;]+)") json_converters = {} -stream = tempfile.NamedTemporaryFile if 'UWSGI_ORIGINAL_PROC_NAME' in os.environ else BytesIO +stream = tempfile.NamedTemporaryFile if "UWSGI_ORIGINAL_PROC_NAME" in os.environ else BytesIO def _json_converter(item): - if hasattr(item, '__native_types__'): + if hasattr(item, "__native_types__"): return item.__native_types__() for kind, transformer in json_converters.items(): @@ -68,10 +110,10 @@ def _json_converter(item): return item.isoformat() elif isinstance(item, bytes): try: - return item.decode('utf8') + return item.decode("utf8") except UnicodeDecodeError: return base64.b64encode(item) - elif hasattr(item, '__iter__'): + elif hasattr(item, "__iter__"): return list(item) elif isinstance(item, (Decimal, UUID)): return str(item) @@ -85,14 +127,17 @@ def json_convert(*kinds): NOTE: custom converters are always globally applied """ + def register_json_converter(function): for kind in kinds: json_converters[kind] = function return function + return register_json_converter if numpy: + @json_convert(numpy.ndarray) def numpy_listable(item): return item.tolist() @@ -118,60 +163,64 @@ def numpy_floatable(item): return float(item) -@content_type('application/json; charset=utf-8') +@content_type("application/json; charset=utf-8") def json(content, request=None, response=None, ensure_ascii=False, **kwargs): """JSON (Javascript Serialized Object Notation)""" - if hasattr(content, 'read'): + if hasattr(content, "read"): return content - if isinstance(content, tuple) and getattr(content, '_fields', None): + if isinstance(content, tuple) and getattr(content, "_fields", None): content = {field: getattr(content, field) for field in content._fields} - return json_converter.dumps(content, default=_json_converter, ensure_ascii=ensure_ascii, **kwargs).encode('utf8') + return json_converter.dumps( + content, default=_json_converter, ensure_ascii=ensure_ascii, **kwargs + ).encode("utf8") def on_valid(valid_content_type, on_invalid=json): """Renders as the specified content type only if no errors are found in the provided data object""" - invalid_kwargs = introspect.generate_accepted_kwargs(on_invalid, 'request', 'response') - invalid_takes_response = introspect.takes_all_arguments(on_invalid, 'response') + invalid_kwargs = introspect.generate_accepted_kwargs(on_invalid, "request", "response") + invalid_takes_response = introspect.takes_all_arguments(on_invalid, "response") def wrapper(function): - valid_kwargs = introspect.generate_accepted_kwargs(function, 'request', 'response') - valid_takes_response = introspect.takes_all_arguments(function, 'response') + valid_kwargs = introspect.generate_accepted_kwargs(function, "request", "response") + valid_takes_response = introspect.takes_all_arguments(function, "response") @content_type(valid_content_type) @wraps(function) def output_content(content, response, **kwargs): - if type(content) == dict and 'errors' in content: + if type(content) == dict and "errors" in content: response.content_type = on_invalid.content_type if invalid_takes_response: - kwargs['response'] = response + kwargs["response"] = response return on_invalid(content, **invalid_kwargs(kwargs)) if valid_takes_response: - kwargs['response'] = response + kwargs["response"] = response return function(content, **valid_kwargs(kwargs)) + return output_content + return wrapper -@content_type('text/plain; charset=utf-8') +@content_type("text/plain; charset=utf-8") def text(content, **kwargs): """Free form UTF-8 text""" - if hasattr(content, 'read'): + if hasattr(content, "read"): return content - return str(content).encode('utf8') + return str(content).encode("utf8") -@content_type('text/html; charset=utf-8') +@content_type("text/html; charset=utf-8") def html(content, **kwargs): """HTML (Hypertext Markup Language)""" - if hasattr(content, 'read'): + if hasattr(content, "read"): return content - elif hasattr(content, 'render'): - return content.render().encode('utf8') + elif hasattr(content, "render"): + return content.render().encode("utf8") - return str(content).encode('utf8') + return str(content).encode("utf8") def _camelcase(content): @@ -191,90 +240,96 @@ def _camelcase(content): return content -@content_type('application/json; charset=utf-8') +@content_type("application/json; charset=utf-8") def json_camelcase(content, **kwargs): """JSON (Javascript Serialized Object Notation) with all keys camelCased""" return json(_camelcase(content), **kwargs) -@content_type('application/json; charset=utf-8') +@content_type("application/json; charset=utf-8") def pretty_json(content, **kwargs): """JSON (Javascript Serialized Object Notion) pretty printed and indented""" - return json(content, indent=4, separators=(',', ': '), **kwargs) + return json(content, indent=4, separators=(",", ": "), **kwargs) def image(image_format, doc=None): """Dynamically creates an image type handler for the specified image type""" - @on_valid('image/{0}'.format(image_format)) + + @on_valid("image/{0}".format(image_format)) def image_handler(data, **kwargs): - if hasattr(data, 'read'): + if hasattr(data, "read"): return data - elif hasattr(data, 'save'): + elif hasattr(data, "save"): output = stream() - if introspect.takes_all_arguments(data.save, 'format') or introspect.takes_kwargs(data.save): + if introspect.takes_all_arguments(data.save, "format") or introspect.takes_kwargs( + data.save + ): data.save(output, format=image_format.upper()) else: data.save(output) output.seek(0) return output - elif hasattr(data, 'render'): + elif hasattr(data, "render"): return data.render() elif os.path.isfile(data): - return open(data, 'rb') + return open(data, "rb") image_handler.__doc__ = doc or "{0} formatted image".format(image_format) return image_handler for image_type in IMAGE_TYPES: - globals()['{0}_image'.format(image_type.replace("+", "_"))] = image(image_type) + globals()["{0}_image".format(image_type.replace("+", "_"))] = image(image_type) def video(video_type, video_mime, doc=None): """Dynamically creates a video type handler for the specified video type""" + @on_valid(video_mime) def video_handler(data, **kwargs): - if hasattr(data, 'read'): + if hasattr(data, "read"): return data - elif hasattr(data, 'save'): + elif hasattr(data, "save"): output = stream() data.save(output, format=video_type.upper()) output.seek(0) return output - elif hasattr(data, 'render'): + elif hasattr(data, "render"): return data.render() elif os.path.isfile(data): - return open(data, 'rb') + return open(data, "rb") video_handler.__doc__ = doc or "{0} formatted video".format(video_type) return video_handler for (video_type, video_mime) in VIDEO_TYPES: - globals()['{0}_video'.format(video_type)] = video(video_type, video_mime) + globals()["{0}_video".format(video_type)] = video(video_type, video_mime) -@on_valid('file/dynamic') +@on_valid("file/dynamic") def file(data, response, **kwargs): """A dynamically retrieved file""" if not data: - response.content_type = 'text/plain' - return '' + response.content_type = "text/plain" + return "" - if hasattr(data, 'read'): - name, data = getattr(data, 'name', ''), data + if hasattr(data, "read"): + name, data = getattr(data, "name", ""), data elif os.path.isfile(data): - name, data = data, open(data, 'rb') + name, data = data, open(data, "rb") else: - response.content_type = 'text/plain' + response.content_type = "text/plain" response.status = HTTP_NOT_FOUND - return 'File not found!' + return "File not found!" - response.content_type = mimetypes.guess_type(name, None)[0] or 'application/octet-stream' + response.content_type = mimetypes.guess_type(name, None)[0] or "application/octet-stream" return data -def on_content_type(handlers, default=None, error='The requested content type does not match any of those allowed'): +def on_content_type( + handlers, default=None, error="The requested content type does not match any of those allowed" +): """Returns a content in a different format based on the clients provided content type, should pass in a dict with the following format: @@ -282,16 +337,19 @@ def on_content_type(handlers, default=None, error='The requested content type do ... } """ + def output_type(data, request, response): - handler = handlers.get(request.content_type.split(';')[0], default) + handler = handlers.get(request.content_type.split(";")[0], default) if not handler: raise falcon.HTTPNotAcceptable(error) response.content_type = handler.content_type return handler(data, request=request, response=response) - output_type.__doc__ = 'Supports any of the following formats: {0}'.format(', '.join( - function.__doc__ or function.__name__ for function in handlers.values())) - output_type.content_type = ', '.join(handlers.keys()) + + output_type.__doc__ = "Supports any of the following formats: {0}".format( + ", ".join(function.__doc__ or function.__name__ for function in handlers.values()) + ) + output_type.content_type = ", ".join(handlers.keys()) return output_type @@ -302,12 +360,14 @@ def accept_quality(accept, default=1): accept, rest = accept.split(";", 1) accept_quality = RE_ACCEPT_QUALITY.search(rest) if accept_quality: - quality = float(accept_quality.groupdict().get('quality', quality).strip()) + quality = float(accept_quality.groupdict().get("quality", quality).strip()) return (quality, accept.strip()) -def accept(handlers, default=None, error='The requested content type does not match any of those allowed'): +def accept( + handlers, default=None, error="The requested content type does not match any of those allowed" +): """Returns a content in a different format based on the clients defined accepted content type, should pass in a dict with the following format: @@ -315,13 +375,14 @@ def accept(handlers, default=None, error='The requested content type does not ma ... } """ + def output_type(data, request, response): accept = request.accept - if accept in ('', '*', '/'): + if accept in ("", "*", "/"): handler = default or handlers and next(iter(handlers.values())) else: handler = default - accepted = [accept_quality(accept_type) for accept_type in accept.split(',')] + accepted = [accept_quality(accept_type) for accept_type in accept.split(",")] accepted.sort(key=itemgetter(0)) for quality, accepted_content_type in reversed(accepted): if accepted_content_type in handlers: @@ -333,13 +394,17 @@ def output_type(data, request, response): response.content_type = handler.content_type return handler(data, request=request, response=response) - output_type.__doc__ = 'Supports any of the following formats: {0}'.format(', '.join(function.__doc__ for function in - handlers.values())) - output_type.content_type = ', '.join(handlers.keys()) + + output_type.__doc__ = "Supports any of the following formats: {0}".format( + ", ".join(function.__doc__ for function in handlers.values()) + ) + output_type.content_type = ", ".join(handlers.keys()) return output_type -def suffix(handlers, default=None, error='The requested suffix does not match any of those allowed'): +def suffix( + handlers, default=None, error="The requested suffix does not match any of those allowed" +): """Returns a content in a different format based on the suffix placed at the end of the URL route should pass in a dict with the following format: @@ -347,6 +412,7 @@ def suffix(handlers, default=None, error='The requested suffix does not match an ... } """ + def output_type(data, request, response): path = request.path handler = default @@ -360,13 +426,17 @@ def output_type(data, request, response): response.content_type = handler.content_type return handler(data, request=request, response=response) - output_type.__doc__ = 'Supports any of the following formats: {0}'.format(', '.join(function.__doc__ for function in - handlers.values())) - output_type.content_type = ', '.join(handlers.keys()) + + output_type.__doc__ = "Supports any of the following formats: {0}".format( + ", ".join(function.__doc__ for function in handlers.values()) + ) + output_type.content_type = ", ".join(handlers.keys()) return output_type -def prefix(handlers, default=None, error='The requested prefix does not match any of those allowed'): +def prefix( + handlers, default=None, error="The requested prefix does not match any of those allowed" +): """Returns a content in a different format based on the prefix placed at the end of the URL route should pass in a dict with the following format: @@ -374,6 +444,7 @@ def prefix(handlers, default=None, error='The requested prefix does not match an ... } """ + def output_type(data, request, response): path = request.path handler = default @@ -387,7 +458,9 @@ def output_type(data, request, response): response.content_type = handler.content_type return handler(data, request=request, response=response) - output_type.__doc__ = 'Supports any of the following formats: {0}'.format(', '.join(function.__doc__ for function in - handlers.values())) - output_type.content_type = ', '.join(handlers.keys()) + + output_type.__doc__ = "Supports any of the following formats: {0}".format( + ", ".join(function.__doc__ for function in handlers.values()) + ) + output_type.content_type = ", ".join(handlers.keys()) return output_type diff --git a/hug/redirect.py b/hug/redirect.py index 79dce31c..f2addd5b 100644 --- a/hug/redirect.py +++ b/hug/redirect.py @@ -26,7 +26,7 @@ def to(location, code=falcon.HTTP_302): """Redirects to the specified location using the provided http_code (defaults to HTTP_302 FOUND)""" - raise falcon.http_status.HTTPStatus(code, {'location': location}) + raise falcon.http_status.HTTPStatus(code, {"location": location}) def permanent(location): diff --git a/hug/route.py b/hug/route.py index 26eb9f10..91eac8a2 100644 --- a/hug/route.py +++ b/hug/route.py @@ -47,7 +47,7 @@ def __call__(self, method_or_class=None, **kwargs): return self.where(**kwargs) if isinstance(method_or_class, (MethodType, FunctionType)): - routes = getattr(method_or_class, '_hug_http_routes', []) + routes = getattr(method_or_class, "_hug_http_routes", []) routes.append(self.route) method_or_class._hug_http_routes = routes return method_or_class @@ -59,11 +59,11 @@ def __call__(self, method_or_class=None, **kwargs): for argument in dir(instance): argument = getattr(instance, argument, None) - http_routes = getattr(argument, '_hug_http_routes', ()) + http_routes = getattr(argument, "_hug_http_routes", ()) for route in http_routes: http(**self.where(**route).route)(argument) - cli_routes = getattr(argument, '_hug_cli_routes', ()) + cli_routes = getattr(argument, "_hug_cli_routes", ()) for route in cli_routes: cli(**self.where(**route).route)(argument) @@ -71,32 +71,36 @@ def __call__(self, method_or_class=None, **kwargs): def http_methods(self, urls=None, **route_data): """Creates routes from a class, where the class method names should line up to HTTP METHOD types""" + def decorator(class_definition): instance = class_definition if isinstance(class_definition, type): instance = class_definition() - router = self.urls(urls if urls else "/{0}".format(instance.__class__.__name__.lower()), **route_data) + router = self.urls( + urls if urls else "/{0}".format(instance.__class__.__name__.lower()), **route_data + ) for method in HTTP_METHODS: handler = getattr(instance, method.lower(), None) if handler: - http_routes = getattr(handler, '_hug_http_routes', ()) + http_routes = getattr(handler, "_hug_http_routes", ()) if http_routes: for route in http_routes: http(**router.accept(method).where(**route).route)(handler) else: http(**router.accept(method).route)(handler) - cli_routes = getattr(handler, '_hug_cli_routes', ()) + cli_routes = getattr(handler, "_hug_cli_routes", ()) if cli_routes: for route in cli_routes: cli(**self.where(**route).route)(handler) return class_definition + return decorator def cli(self, method): """Registers a method on an Object as a CLI route""" - routes = getattr(method, '_hug_cli_routes', []) + routes = getattr(method, "_hug_cli_routes", []) routes.append(self.route) method._hug_cli_routes = routes return method @@ -104,7 +108,8 @@ def cli(self, method): class API(object): """Provides a convient way to route functions to a single API independent of where they live""" - __slots__ = ('api', ) + + __slots__ = ("api",) def __init__(self, api): if type(api) == str: @@ -113,7 +118,7 @@ def __init__(self, api): def http(self, *args, **kwargs): """Starts the process of building a new HTTP route linked to this API instance""" - kwargs['api'] = self.api + kwargs["api"] = self.api return http(*args, **kwargs) def urls(self, *args, **kwargs): @@ -125,110 +130,112 @@ def urls(self, *args, **kwargs): def not_found(self, *args, **kwargs): """Defines the handler that should handle not found requests against this API""" - kwargs['api'] = self.api + kwargs["api"] = self.api return not_found(*args, **kwargs) def static(self, *args, **kwargs): """Define the routes to static files the API should expose""" - kwargs['api'] = self.api + kwargs["api"] = self.api return static(*args, **kwargs) def sink(self, *args, **kwargs): """Define URL prefixes/handler matches where everything under the URL prefix should be handled""" - kwargs['api'] = self.api + kwargs["api"] = self.api return sink(*args, **kwargs) def exception(self, *args, **kwargs): """Defines how this API should handle the provided exceptions""" - kwargs['api'] = self.api + kwargs["api"] = self.api return exception(*args, **kwargs) def cli(self, *args, **kwargs): """Defines a CLI function that should be routed by this API""" - kwargs['api'] = self.api + kwargs["api"] = self.api return cli(*args, **kwargs) def object(self, *args, **kwargs): """Registers a class based router to this API""" - kwargs['api'] = self.api + kwargs["api"] = self.api return Object(*args, **kwargs) def get(self, *args, **kwargs): """Builds a new GET HTTP route that is registered to this API""" - kwargs['api'] = self.api - kwargs['accept'] = ('GET', ) + kwargs["api"] = self.api + kwargs["accept"] = ("GET",) return http(*args, **kwargs) def post(self, *args, **kwargs): """Builds a new POST HTTP route that is registered to this API""" - kwargs['api'] = self.api - kwargs['accept'] = ('POST', ) + kwargs["api"] = self.api + kwargs["accept"] = ("POST",) return http(*args, **kwargs) def put(self, *args, **kwargs): """Builds a new PUT HTTP route that is registered to this API""" - kwargs['api'] = self.api - kwargs['accept'] = ('PUT', ) + kwargs["api"] = self.api + kwargs["accept"] = ("PUT",) return http(*args, **kwargs) def delete(self, *args, **kwargs): """Builds a new DELETE HTTP route that is registered to this API""" - kwargs['api'] = self.api - kwargs['accept'] = ('DELETE', ) + kwargs["api"] = self.api + kwargs["accept"] = ("DELETE",) return http(*args, **kwargs) def connect(self, *args, **kwargs): """Builds a new CONNECT HTTP route that is registered to this API""" - kwargs['api'] = self.api - kwargs['accept'] = ('CONNECT', ) + kwargs["api"] = self.api + kwargs["accept"] = ("CONNECT",) return http(*args, **kwargs) def head(self, *args, **kwargs): """Builds a new HEAD HTTP route that is registered to this API""" - kwargs['api'] = self.api - kwargs['accept'] = ('HEAD', ) + kwargs["api"] = self.api + kwargs["accept"] = ("HEAD",) return http(*args, **kwargs) def options(self, *args, **kwargs): """Builds a new OPTIONS HTTP route that is registered to this API""" - kwargs['api'] = self.api - kwargs['accept'] = ('OPTIONS', ) + kwargs["api"] = self.api + kwargs["accept"] = ("OPTIONS",) return http(*args, **kwargs) def patch(self, *args, **kwargs): """Builds a new PATCH HTTP route that is registered to this API""" - kwargs['api'] = self.api - kwargs['accept'] = ('PATCH', ) + kwargs["api"] = self.api + kwargs["accept"] = ("PATCH",) return http(*args, **kwargs) def trace(self, *args, **kwargs): """Builds a new TRACE HTTP route that is registered to this API""" - kwargs['api'] = self.api - kwargs['accept'] = ('TRACE', ) + kwargs["api"] = self.api + kwargs["accept"] = ("TRACE",) return http(*args, **kwargs) def get_post(self, *args, **kwargs): """Builds a new GET or POST HTTP route that is registered to this API""" - kwargs['api'] = self.api - kwargs['accept'] = ('GET', 'POST') + kwargs["api"] = self.api + kwargs["accept"] = ("GET", "POST") return http(*args, **kwargs) def put_post(self, *args, **kwargs): """Builds a new PUT or POST HTTP route that is registered to this API""" - kwargs['api'] = self.api - kwargs['accept'] = ('PUT', 'POST') + kwargs["api"] = self.api + kwargs["accept"] = ("PUT", "POST") return http(*args, **kwargs) for method in HTTP_METHODS: - method_handler = partial(http, accept=(method, )) - method_handler.__doc__ = "Exposes a Python method externally as an HTTP {0} method".format(method.upper()) + method_handler = partial(http, accept=(method,)) + method_handler.__doc__ = "Exposes a Python method externally as an HTTP {0} method".format( + method.upper() + ) globals()[method.lower()] = method_handler -get_post = partial(http, accept=('GET', 'POST')) +get_post = partial(http, accept=("GET", "POST")) get_post.__doc__ = "Exposes a Python method externally under both the HTTP POST and GET methods" -put_post = partial(http, accept=('PUT', 'POST')) +put_post = partial(http, accept=("PUT", "POST")) put_post.__doc__ = "Exposes a Python method externally under both the HTTP POST and PUT methods" object = Object() diff --git a/hug/routing.py b/hug/routing.py index c05f8ce8..e215f257 100644 --- a/hug/routing.py +++ b/hug/routing.py @@ -40,25 +40,37 @@ class Router(object): """The base chainable router object""" - __slots__ = ('route', ) - def __init__(self, transform=None, output=None, validate=None, api=None, requires=(), map_params=None, - args=None, **kwargs): + __slots__ = ("route",) + + def __init__( + self, + transform=None, + output=None, + validate=None, + api=None, + requires=(), + map_params=None, + args=None, + **kwargs + ): self.route = {} if transform is not None: - self.route['transform'] = transform + self.route["transform"] = transform if output: - self.route['output'] = output + self.route["output"] = output if validate: - self.route['validate'] = validate + self.route["validate"] = validate if api: - self.route['api'] = api + self.route["api"] = api if requires: - self.route['requires'] = (requires, ) if not isinstance(requires, (tuple, list)) else requires + self.route["requires"] = ( + (requires,) if not isinstance(requires, (tuple, list)) else requires + ) if map_params: - self.route['map_params'] = map_params + self.route["map_params"] = map_params if args: - self.route['args'] = args + self.route["args"] = args def output(self, formatter, **overrides): """Sets the output formatter that should be used to render this route""" @@ -80,12 +92,19 @@ def api(self, api, **overrides): def requires(self, requirements, **overrides): """Adds additional requirements to the specified route""" - return self.where(requires=tuple(self.route.get('requires', ())) + tuple(requirements), **overrides) + return self.where( + requires=tuple(self.route.get("requires", ())) + tuple(requirements), **overrides + ) def doesnt_require(self, requirements, **overrides): """Removes individual requirements while keeping all other defined ones within a route""" - return self.where(requires=tuple(set(self.route.get('requires', ())).difference(requirements if - type(requirements) in (list, tuple) else (requirements, )))) + return self.where( + requires=tuple( + set(self.route.get("requires", ())).difference( + requirements if type(requirements) in (list, tuple) else (requirements,) + ) + ) + ) def map_params(self, **map_params): """Map interface specific params to an internal name representation""" @@ -100,16 +119,17 @@ def where(self, **overrides): class CLIRouter(Router): """The CLIRouter provides a chainable router that can be used to route a CLI command to a Python function""" + __slots__ = () def __init__(self, name=None, version=None, doc=None, **kwargs): super().__init__(**kwargs) if name is not None: - self.route['name'] = name + self.route["name"] = name if version: - self.route['version'] = version + self.route["version"] = version if doc: - self.route['doc'] = doc + self.route["doc"] = doc def name(self, name, **overrides): """Sets the name for the CLI interface""" @@ -131,16 +151,17 @@ def __call__(self, api_function): class InternalValidation(Router): """Defines the base route for interfaces that define their own internal validation""" + __slots__ = () def __init__(self, raise_on_invalid=False, on_invalid=None, output_invalid=None, **kwargs): super().__init__(**kwargs) if raise_on_invalid: - self.route['raise_on_invalid'] = raise_on_invalid + self.route["raise_on_invalid"] = raise_on_invalid if on_invalid is not None: - self.route['on_invalid'] = on_invalid + self.route["on_invalid"] = on_invalid if output_invalid is not None: - self.route['output_invalid'] = output_invalid + self.route["output_invalid"] = output_invalid def raise_on_invalid(self, setting=True, **overrides): """Sets the route to raise validation errors instead of catching them""" @@ -164,16 +185,17 @@ def output_invalid(self, output_handler, **overrides): class LocalRouter(InternalValidation): """The LocalRouter defines how interfaces should be handled when accessed locally from within Python code""" + __slots__ = () def __init__(self, directives=True, validate=True, version=None, **kwargs): super().__init__(**kwargs) if version is not None: - self.route['version'] = version + self.route["version"] = version if not directives: - self.route['skip_directives'] = True + self.route["skip_directives"] = True if not validate: - self.route['skip_validation'] = True + self.route["skip_validation"] = True def directives(self, use=True, **kwargs): return self.where(directives=use) @@ -191,28 +213,43 @@ def __call__(self, api_function): class HTTPRouter(InternalValidation): """The HTTPRouter provides the base concept of a router from an HTTPRequest to a Python function""" + __slots__ = () - def __init__(self, versions=any, parse_body=False, parameters=None, defaults={}, status=None, - response_headers=None, private=False, inputs=None, **kwargs): + def __init__( + self, + versions=any, + parse_body=False, + parameters=None, + defaults={}, + status=None, + response_headers=None, + private=False, + inputs=None, + **kwargs + ): super().__init__(**kwargs) if versions is not any: - self.route['versions'] = (versions, ) if isinstance(versions, (int, float, None.__class__)) else versions - self.route['versions'] = tuple(int(version) if version else version for version in self.route['versions']) + self.route["versions"] = ( + (versions,) if isinstance(versions, (int, float, None.__class__)) else versions + ) + self.route["versions"] = tuple( + int(version) if version else version for version in self.route["versions"] + ) if parse_body: - self.route['parse_body'] = parse_body + self.route["parse_body"] = parse_body if parameters: - self.route['parameters'] = parameters + self.route["parameters"] = parameters if defaults: - self.route['defaults'] = defaults + self.route["defaults"] = defaults if status: - self.route['status'] = status + self.route["status"] = status if response_headers: - self.route['response_headers'] = response_headers + self.route["response_headers"] = response_headers if private: - self.route['private'] = private + self.route["private"] = private if inputs: - self.route['inputs'] = inputs + self.route["inputs"] = inputs def versions(self, supported, **overrides): """Sets the versions that this route should be compatiable with""" @@ -244,53 +281,73 @@ def response_headers(self, headers, **overrides): def add_response_headers(self, headers, **overrides): """Adds the specified response headers while keeping existing ones in-tact""" - response_headers = self.route.get('response_headers', {}).copy() + response_headers = self.route.get("response_headers", {}).copy() response_headers.update(headers) return self.where(response_headers=response_headers, **overrides) - def cache(self, private=False, max_age=31536000, s_maxage=None, no_cache=False, no_store=False, - must_revalidate=False, **overrides): + def cache( + self, + private=False, + max_age=31536000, + s_maxage=None, + no_cache=False, + no_store=False, + must_revalidate=False, + **overrides + ): """Convenience method for quickly adding cache header to route""" - parts = ('private' if private else 'public', 'max-age={0}'.format(max_age), - 's-maxage={0}'.format(s_maxage) if s_maxage is not None else None, no_cache and 'no-cache', - no_store and 'no-store', must_revalidate and 'must-revalidate') - return self.add_response_headers({'cache-control': ', '.join(filter(bool, parts))}, **overrides) - - def allow_origins(self, *origins, methods=None, max_age=None, credentials=None, headers=None, **overrides): + parts = ( + "private" if private else "public", + "max-age={0}".format(max_age), + "s-maxage={0}".format(s_maxage) if s_maxage is not None else None, + no_cache and "no-cache", + no_store and "no-store", + must_revalidate and "must-revalidate", + ) + return self.add_response_headers( + {"cache-control": ", ".join(filter(bool, parts))}, **overrides + ) + + def allow_origins( + self, *origins, methods=None, max_age=None, credentials=None, headers=None, **overrides + ): """Convenience method for quickly allowing other resources to access this one""" response_headers = {} if origins: + @hug.response_middleware() def process_data(request, response, resource): - if 'ORIGIN' in request.headers: - origin = request.headers['ORIGIN'] + if "ORIGIN" in request.headers: + origin = request.headers["ORIGIN"] if origin in origins: - response.set_header('Access-Control-Allow-Origin', origin) + response.set_header("Access-Control-Allow-Origin", origin) + else: - response_headers['Access-Control-Allow-Origin'] = '*' + response_headers["Access-Control-Allow-Origin"] = "*" if methods: - response_headers['Access-Control-Allow-Methods'] = ', '.join(methods) + response_headers["Access-Control-Allow-Methods"] = ", ".join(methods) if max_age: - response_headers['Access-Control-Max-Age'] = max_age + response_headers["Access-Control-Max-Age"] = max_age if credentials: - response_headers['Access-Control-Allow-Credentials'] = str(credentials).lower() + response_headers["Access-Control-Allow-Credentials"] = str(credentials).lower() if headers: - response_headers['Access-Control-Allow-Headers'] = headers + response_headers["Access-Control-Allow-Headers"] = headers return self.add_response_headers(response_headers, **overrides) class NotFoundRouter(HTTPRouter): """Provides a chainable router that can be used to route 404'd request to a Python function""" + __slots__ = () def __init__(self, output=None, versions=any, status=falcon.HTTP_NOT_FOUND, **kwargs): super().__init__(output=output, versions=versions, status=status, **kwargs) def __call__(self, api_function): - api = self.route.get('api', hug.api.from_object(api_function)) + api = self.route.get("api", hug.api.from_object(api_function)) (interface, callable_method) = self._create_interface(api, api_function) - for version in self.route.get('versions', (None, )): + for version in self.route.get("versions", (None,)): api.http.set_not_found_handler(interface, version) return callable_method @@ -298,24 +355,26 @@ def __call__(self, api_function): class SinkRouter(HTTPRouter): """Provides a chainable router that can be used to route all routes pass a certain base URL (essentially route/*)""" + __slots__ = () def __init__(self, urls=None, output=None, **kwargs): super().__init__(output=output, **kwargs) if urls: - self.route['urls'] = (urls, ) if isinstance(urls, str) else urls + self.route["urls"] = (urls,) if isinstance(urls, str) else urls def __call__(self, api_function): - api = self.route.get('api', hug.api.from_object(api_function)) + api = self.route.get("api", hug.api.from_object(api_function)) (interface, callable_method) = self._create_interface(api, api_function) - for base_url in self.route.get('urls', ("/{0}".format(api_function.__name__), )): + for base_url in self.route.get("urls", ("/{0}".format(api_function.__name__),)): api.http.add_sink(interface, base_url) return callable_method class StaticRouter(SinkRouter): """Provides a chainable router that can be used to return static files automatically from a set of directories""" - __slots__ = ('route', ) + + __slots__ = ("route",) def __init__(self, urls=None, output=hug.output_format.file, cache=False, **kwargs): super().__init__(urls=urls, output=output, **kwargs) @@ -327,13 +386,12 @@ def __init__(self, urls=None, output=hug.output_format.file, cache=False, **kwar def __call__(self, api_function): directories = [] for directory in api_function(): - path = os.path.abspath( - directory - ) + path = os.path.abspath(directory) directories.append(path) - api = self.route.get('api', hug.api.from_object(api_function)) - for base_url in self.route.get('urls', ("/{0}".format(api_function.__name__), )): + api = self.route.get("api", hug.api.from_object(api_function)) + for base_url in self.route.get("urls", ("/{0}".format(api_function.__name__),)): + def read_file(request=None, path=""): filename = path.lstrip("/") for directory in directories: @@ -348,24 +406,30 @@ def read_file(request=None, path=""): return path hug.redirect.not_found() + api.http.add_sink(self._create_interface(api, read_file)[0], base_url) return api_function class ExceptionRouter(HTTPRouter): """Provides a chainable router that can be used to route exceptions thrown during request handling""" + __slots__ = () - def __init__(self, exceptions=(Exception, ), exclude=(), output=None, **kwargs): + def __init__(self, exceptions=(Exception,), exclude=(), output=None, **kwargs): super().__init__(output=output, **kwargs) - self.route['exceptions'] = (exceptions, ) if not isinstance(exceptions, (list, tuple)) else exceptions - self.route['exclude'] = (exclude, ) if not isinstance(exclude, (list, tuple)) else exclude + self.route["exceptions"] = ( + (exceptions,) if not isinstance(exceptions, (list, tuple)) else exceptions + ) + self.route["exclude"] = (exclude,) if not isinstance(exclude, (list, tuple)) else exclude def __call__(self, api_function): - api = self.route.get('api', hug.api.from_object(api_function)) - (interface, callable_method) = self._create_interface(api, api_function, catch_exceptions=False) - for version in self.route.get('versions', (None, )): - for exception in self.route['exceptions']: + api = self.route.get("api", hug.api.from_object(api_function)) + (interface, callable_method) = self._create_interface( + api, api_function, catch_exceptions=False + ) + for version in self.route.get("versions", (None,)): + for exception in self.route["exceptions"]: api.http.add_exception_handler(exception, interface, version) return callable_method @@ -377,48 +441,67 @@ def _create_interface(self, api, api_function, catch_exceptions=False): class URLRouter(HTTPRouter): """Provides a chainable router that can be used to route a URL to a Python function""" + __slots__ = () - def __init__(self, urls=None, accept=HTTP_METHODS, output=None, examples=(), versions=any, - suffixes=(), prefixes=(), response_headers=None, parse_body=True, **kwargs): - super().__init__(output=output, versions=versions, parse_body=parse_body, response_headers=response_headers, - **kwargs) + def __init__( + self, + urls=None, + accept=HTTP_METHODS, + output=None, + examples=(), + versions=any, + suffixes=(), + prefixes=(), + response_headers=None, + parse_body=True, + **kwargs + ): + super().__init__( + output=output, + versions=versions, + parse_body=parse_body, + response_headers=response_headers, + **kwargs + ) if urls is not None: - self.route['urls'] = (urls, ) if isinstance(urls, str) else urls + self.route["urls"] = (urls,) if isinstance(urls, str) else urls if accept: - self.route['accept'] = (accept, ) if isinstance(accept, str) else accept + self.route["accept"] = (accept,) if isinstance(accept, str) else accept if examples: - self.route['examples'] = (examples, ) if isinstance(examples, str) else examples + self.route["examples"] = (examples,) if isinstance(examples, str) else examples if suffixes: - self.route['suffixes'] = (suffixes, ) if isinstance(suffixes, str) else suffixes + self.route["suffixes"] = (suffixes,) if isinstance(suffixes, str) else suffixes if prefixes: - self.route['prefixes'] = (prefixes, ) if isinstance(prefixes, str) else prefixes + self.route["prefixes"] = (prefixes,) if isinstance(prefixes, str) else prefixes def __call__(self, api_function): - api = self.route.get('api', hug.api.from_object(api_function)) + api = self.route.get("api", hug.api.from_object(api_function)) api.http.routes.setdefault(api.http.base_url, OrderedDict()) (interface, callable_method) = self._create_interface(api, api_function) - use_examples = self.route.get('examples', ()) + use_examples = self.route.get("examples", ()) if not interface.required and not use_examples: - use_examples = (True, ) + use_examples = (True,) - for base_url in self.route.get('urls', ("/{0}".format(api_function.__name__), )): - expose = [base_url, ] - for suffix in self.route.get('suffixes', ()): - if suffix.startswith('/'): - expose.append(os.path.join(base_url, suffix.lstrip('/'))) + for base_url in self.route.get("urls", ("/{0}".format(api_function.__name__),)): + expose = [base_url] + for suffix in self.route.get("suffixes", ()): + if suffix.startswith("/"): + expose.append(os.path.join(base_url, suffix.lstrip("/"))) else: expose.append(base_url + suffix) - for prefix in self.route.get('prefixes', ()): + for prefix in self.route.get("prefixes", ()): expose.append(prefix + base_url) for url in expose: handlers = api.http.routes[api.http.base_url].setdefault(url, {}) - for method in self.route.get('accept', ()): + for method in self.route.get("accept", ()): version_mapping = handlers.setdefault(method.upper(), {}) - for version in self.route.get('versions', (None, )): + for version in self.route.get("versions", (None,)): version_mapping[version] = interface - api.http.versioned.setdefault(version, {})[callable_method.__name__] = callable_method + api.http.versioned.setdefault(version, {})[ + callable_method.__name__ + ] = callable_method interface.examples = use_examples return callable_method @@ -434,56 +517,56 @@ def accept(self, *accept, **overrides): def get(self, urls=None, **overrides): """Sets the acceptable HTTP method to a GET""" if urls is not None: - overrides['urls'] = urls - return self.where(accept='GET', **overrides) + overrides["urls"] = urls + return self.where(accept="GET", **overrides) def delete(self, urls=None, **overrides): """Sets the acceptable HTTP method to DELETE""" if urls is not None: - overrides['urls'] = urls - return self.where(accept='DELETE', **overrides) + overrides["urls"] = urls + return self.where(accept="DELETE", **overrides) def post(self, urls=None, **overrides): """Sets the acceptable HTTP method to POST""" if urls is not None: - overrides['urls'] = urls - return self.where(accept='POST', **overrides) + overrides["urls"] = urls + return self.where(accept="POST", **overrides) def put(self, urls=None, **overrides): """Sets the acceptable HTTP method to PUT""" if urls is not None: - overrides['urls'] = urls - return self.where(accept='PUT', **overrides) + overrides["urls"] = urls + return self.where(accept="PUT", **overrides) def trace(self, urls=None, **overrides): """Sets the acceptable HTTP method to TRACE""" if urls is not None: - overrides['urls'] = urls - return self.where(accept='TRACE', **overrides) + overrides["urls"] = urls + return self.where(accept="TRACE", **overrides) def patch(self, urls=None, **overrides): """Sets the acceptable HTTP method to PATCH""" if urls is not None: - overrides['urls'] = urls - return self.where(accept='PATCH', **overrides) + overrides["urls"] = urls + return self.where(accept="PATCH", **overrides) def options(self, urls=None, **overrides): """Sets the acceptable HTTP method to OPTIONS""" if urls is not None: - overrides['urls'] = urls - return self.where(accept='OPTIONS', **overrides) + overrides["urls"] = urls + return self.where(accept="OPTIONS", **overrides) def head(self, urls=None, **overrides): """Sets the acceptable HTTP method to HEAD""" if urls is not None: - overrides['urls'] = urls - return self.where(accept='HEAD', **overrides) + overrides["urls"] = urls + return self.where(accept="HEAD", **overrides) def connect(self, urls=None, **overrides): """Sets the acceptable HTTP method to CONNECT""" if urls is not None: - overrides['urls'] = urls - return self.where(accept='CONNECT', **overrides) + overrides["urls"] = urls + return self.where(accept="CONNECT", **overrides) def call(self, **overrides): """Sets the acceptable HTTP method to all known""" @@ -495,11 +578,11 @@ def http(self, **overrides): def get_post(self, **overrides): """Exposes a Python method externally under both the HTTP POST and GET methods""" - return self.where(accept=('GET', 'POST'), **overrides) + return self.where(accept=("GET", "POST"), **overrides) def put_post(self, **overrides): """Exposes a Python method externally under both the HTTP POST and PUT methods""" - return self.where(accept=('PUT', 'POST'), **overrides) + return self.where(accept=("PUT", "POST"), **overrides) def examples(self, *examples, **overrides): """Sets the examples that the route should use""" @@ -514,15 +597,17 @@ def prefixes(self, *prefixes, **overrides): return self.where(prefixes=prefixes, **overrides) def where(self, **overrides): - if 'urls' in overrides: - existing_urls = self.route.get('urls', ()) + if "urls" in overrides: + existing_urls = self.route.get("urls", ()) use_urls = [] - for url in (overrides['urls'], ) if isinstance(overrides['urls'], str) else overrides['urls']: - if url.startswith('/') or not existing_urls: + for url in ( + (overrides["urls"],) if isinstance(overrides["urls"], str) else overrides["urls"] + ): + if url.startswith("/") or not existing_urls: use_urls.append(url) else: for existing in existing_urls: - use_urls.append(urljoin(existing.rstrip('/') + '/', url)) - overrides['urls'] = tuple(use_urls) + use_urls.append(urljoin(existing.rstrip("/") + "/", url)) + overrides["urls"] = tuple(use_urls) return super().where(**overrides) diff --git a/hug/store.py b/hug/store.py index c1b7ed19..0f28b346 100644 --- a/hug/store.py +++ b/hug/store.py @@ -29,6 +29,7 @@ class InMemoryStore: Regard this as a blueprint for more useful and probably more complex store implementations, for example stores which make use of databases like Redis, PostgreSQL or others. """ + def __init__(self): self._data = {} diff --git a/hug/test.py b/hug/test.py index 5e7c91f3..3f1bb113 100644 --- a/hug/test.py +++ b/hug/test.py @@ -38,53 +38,77 @@ def _internal_result(raw_response): try: - return raw_response[0].decode('utf8') + return raw_response[0].decode("utf8") except TypeError: data = BytesIO() for chunk in raw_response: data.write(chunk) data = data.getvalue() try: - return data.decode('utf8') - except UnicodeDecodeError: # pragma: no cover + return data.decode("utf8") + except UnicodeDecodeError: # pragma: no cover return data except (UnicodeDecodeError, AttributeError): return raw_response[0] -def call(method, api_or_module, url, body='', headers=None, params=None, query_string='', scheme='http', - host=DEFAULT_HOST, **kwargs): +def call( + method, + api_or_module, + url, + body="", + headers=None, + params=None, + query_string="", + scheme="http", + host=DEFAULT_HOST, + **kwargs +): """Simulates a round-trip call against the given API / URL""" api = API(api_or_module).http.server() response = StartResponseMock() headers = {} if headers is None else headers - if not isinstance(body, str) and 'json' in headers.get('content-type', 'application/json'): + if not isinstance(body, str) and "json" in headers.get("content-type", "application/json"): body = output_format.json(body) - headers.setdefault('content-type', 'application/json') + headers.setdefault("content-type", "application/json") params = params if params else {} params.update(kwargs) if params: - query_string = '{}{}{}'.format(query_string, '&' if query_string else '', urlencode(params, True)) - result = api(create_environ(path=url, method=method, headers=headers, query_string=query_string, - body=body, scheme=scheme, host=host), response) + query_string = "{}{}{}".format( + query_string, "&" if query_string else "", urlencode(params, True) + ) + result = api( + create_environ( + path=url, + method=method, + headers=headers, + query_string=query_string, + body=body, + scheme=scheme, + host=host, + ), + response, + ) if result: response.data = _internal_result(result) - response.content_type = response.headers_dict['content-type'] - if 'application/json' in response.content_type: + response.content_type = response.headers_dict["content-type"] + if "application/json" in response.content_type: response.data = json.loads(response.data) return response for method in HTTP_METHODS: tester = partial(call, method) - tester.__doc__ = """Simulates a round-trip HTTP {0} against the given API / URL""".format(method.upper()) + tester.__doc__ = """Simulates a round-trip HTTP {0} against the given API / URL""".format( + method.upper() + ) globals()[method.lower()] = tester def cli(method, *args, api=None, module=None, **arguments): """Simulates testing a hug cli method from the command line""" - collect_output = arguments.pop('collect_output', True) + collect_output = arguments.pop("collect_output", True) if api and module: raise ValueError("Please specify an API OR a Module that contains the API, not both") elif api or module: @@ -93,11 +117,11 @@ def cli(method, *args, api=None, module=None, **arguments): command_args = [method.__name__] + list(args) for name, values in arguments.items(): if not isinstance(values, (tuple, list)): - values = (values, ) + values = (values,) for value in values: - command_args.append('--{0}'.format(name)) + command_args.append("--{0}".format(name)) if not value in (True, False): - command_args.append('{0}'.format(value)) + command_args.append("{0}".format(value)) old_sys_argv = sys.argv sys.argv = [str(part) for part in command_args] @@ -110,7 +134,7 @@ def cli(method, *args, api=None, module=None, **arguments): try: method.interface.cli() except Exception as e: - to_return = (e, ) + to_return = (e,) method.interface.cli.outputs = old_outputs sys.argv = old_sys_argv diff --git a/hug/transform.py b/hug/transform.py index 548d75cc..e002cb6a 100644 --- a/hug/transform.py +++ b/hug/transform.py @@ -34,16 +34,19 @@ def content_type(transformers, default=None): ... } """ - transformers = {content_type: auto_kwargs(transformer) if transformer else transformer - for content_type, transformer in transformers.items()} + transformers = { + content_type: auto_kwargs(transformer) if transformer else transformer + for content_type, transformer in transformers.items() + } default = default and auto_kwargs(default) def transform(data, request): - transformer = transformers.get(request.content_type.split(';')[0], default) + transformer = transformers.get(request.content_type.split(";")[0], default) if not transformer: return data return transformer(data) + return transform @@ -57,8 +60,10 @@ def suffix(transformers, default=None): ... } """ - transformers = {suffix: auto_kwargs(transformer) if transformer - else transformer for suffix, transformer in transformers.items()} + transformers = { + suffix: auto_kwargs(transformer) if transformer else transformer + for suffix, transformer in transformers.items() + } default = default and auto_kwargs(default) def transform(data, request): @@ -70,6 +75,7 @@ def transform(data, request): break return transformer(data) if transformer else data + return transform @@ -83,8 +89,10 @@ def prefix(transformers, default=None): ... } """ - transformers = {prefix: auto_kwargs(transformer) if transformer else transformer - for prefix, transformer in transformers.items()} + transformers = { + prefix: auto_kwargs(transformer) if transformer else transformer + for prefix, transformer in transformers.items() + } default = default and auto_kwargs(default) def transform(data, request=None, response=None): @@ -96,6 +104,7 @@ def transform(data, request=None, response=None): break return transformer(data) if transformer else data + return transform @@ -113,4 +122,5 @@ def transform(data, request=None, response=None): data = transformer(data, request=request, response=response) return data + return transform diff --git a/hug/types.py b/hug/types.py index 4d98743a..666fb82e 100644 --- a/hug/types.py +++ b/hug/types.py @@ -33,6 +33,7 @@ try: import marshmallow from marshmallow import ValidationError + MARSHMALLOW_MAJOR_VERSION = marshmallow.__version_info__[0] except ImportError: # Just define the error that is never raised so that Python does not complain. @@ -44,6 +45,7 @@ class Type(object): """Defines the base hug concept of a type for use in function annotation. Override `__call__` to define how the type should be transformed and validated """ + _hug_type = True _sub_type = None _accept_context = False @@ -52,11 +54,18 @@ def __init__(self): pass def __call__(self, value): - raise NotImplementedError('To implement a new type __call__ must be defined') - - -def create(doc=None, error_text=None, exception_handlers=empty.dict, extend=Type, chain=True, auto_instance=True, - accept_context=False): + raise NotImplementedError("To implement a new type __call__ must be defined") + + +def create( + doc=None, + error_text=None, + exception_handlers=empty.dict, + extend=Type, + chain=True, + auto_instance=True, + accept_context=False, +): """Creates a new type handler with the specified type-casting handler""" extend = extend if type(extend) == type else type(extend) @@ -68,6 +77,7 @@ class NewType(extend): if chain and extend != Type: if error_text or exception_handlers: if not accept_context: + def __call__(self, value): try: value = super(NewType, self).__call__(value) @@ -82,8 +92,10 @@ def __call__(self, value): if error_text: raise ValueError(error_text) raise exception + else: if extend._accept_context: + def __call__(self, value, context): try: value = super(NewType, self).__call__(value, context) @@ -98,7 +110,9 @@ def __call__(self, value, context): if error_text: raise ValueError(error_text) raise exception + else: + def __call__(self, value, context): try: value = super(NewType, self).__call__(value) @@ -113,23 +127,31 @@ def __call__(self, value, context): if error_text: raise ValueError(error_text) raise exception + else: if not accept_context: + def __call__(self, value): value = super(NewType, self).__call__(value) return function(value) + else: if extend._accept_context: + def __call__(self, value, context): value = super(NewType, self).__call__(value, context) return function(value, context) + else: + def __call__(self, value, context): value = super(NewType, self).__call__(value) return function(value, context) + else: if not accept_context: if error_text or exception_handlers: + def __call__(self, value): try: return function(value) @@ -143,11 +165,15 @@ def __call__(self, value): if error_text: raise ValueError(error_text) raise exception + else: + def __call__(self, value): return function(value) + else: if error_text or exception_handlers: + def __call__(self, value, context): try: return function(value, context) @@ -161,14 +187,18 @@ def __call__(self, value, context): if error_text: raise ValueError(error_text) raise exception + else: + def __call__(self, value, context): return function(value, context) NewType.__doc__ = function.__doc__ if doc is None else doc - if auto_instance and not (introspect.arguments(NewType.__init__, -1) or - introspect.takes_kwargs(NewType.__init__) or - introspect.takes_args(NewType.__init__)): + if auto_instance and not ( + introspect.arguments(NewType.__init__, -1) + or introspect.takes_kwargs(NewType.__init__) + or introspect.takes_args(NewType.__init__) + ): return NewType() return NewType @@ -182,25 +212,30 @@ def accept(kind, doc=None, error_text=None, exception_handlers=empty.dict, accep error_text, exception_handlers=exception_handlers, chain=False, - accept_context=accept_context + accept_context=accept_context, )(kind) -number = accept(int, 'A whole number', 'Invalid whole number provided') -float_number = accept(float, 'A float number', 'Invalid float number provided') -decimal = accept(Decimal, 'A decimal number', 'Invalid decimal number provided') -boolean = accept(bool, 'Providing any value will set this to true', 'Invalid boolean value provided') -uuid = accept(native_uuid.UUID, 'A Universally Unique IDentifier', 'Invalid UUID provided') + +number = accept(int, "A whole number", "Invalid whole number provided") +float_number = accept(float, "A float number", "Invalid float number provided") +decimal = accept(Decimal, "A decimal number", "Invalid decimal number provided") +boolean = accept( + bool, "Providing any value will set this to true", "Invalid boolean value provided" +) +uuid = accept(native_uuid.UUID, "A Universally Unique IDentifier", "Invalid UUID provided") class Text(Type): """Basic text / string value""" + __slots__ = () def __call__(self, value): if type(value) in (list, tuple) or value is None: - raise ValueError('Invalid text value provided') + raise ValueError("Invalid text value provided") return str(value) + text = Text() @@ -208,11 +243,13 @@ class SubTyped(type): def __getitem__(cls, sub_type): class TypedSubclass(cls): _sub_type = sub_type + return TypedSubclass class Multiple(Type, metaclass=SubTyped): """Multiple Values""" + __slots__ = () def __call__(self, value): @@ -222,9 +259,9 @@ def __call__(self, value): return as_multiple - class DelimitedList(Type, metaclass=SubTyped): """Defines a list type that is formed by delimiting a list with a certain character or set of characters""" + def __init__(self, using=","): super().__init__() self.using = using @@ -242,6 +279,7 @@ def __call__(self, value): class SmartBoolean(type(boolean)): """Accepts a true or false value""" + __slots__ = () def __call__(self, value): @@ -249,12 +287,12 @@ def __call__(self, value): return bool(value) value = value.lower() - if value in ('true', 't', '1'): + if value in ("true", "t", "1"): return True - elif value in ('false', 'f', '0', ''): + elif value in ("false", "f", "0", ""): return False - raise KeyError('Invalid value passed in for true/false field') + raise KeyError("Invalid value passed in for true/false field") class InlineDictionary(Type, metaclass=SubTyped): @@ -274,31 +312,36 @@ def __call__(self, string): dictionary = {} for key, value in (item.split(":") for item in string.split("|")): key, value = key.strip(), value.strip() - dictionary[self.key_type(key) - if self.key_type else key] = self.value_type(value) if self.value_type else value + dictionary[self.key_type(key) if self.key_type else key] = ( + self.value_type(value) if self.value_type else value + ) return dictionary class OneOf(Type): """Ensures the value is within a set of acceptable values""" - __slots__ = ('values', ) + + __slots__ = ("values",) def __init__(self, values): self.values = values @property def __doc__(self): - return 'Accepts one of the following values: ({0})'.format("|".join(self.values)) + return "Accepts one of the following values: ({0})".format("|".join(self.values)) def __call__(self, value): if not value in self.values: - raise KeyError('Invalid value passed. The accepted values are: ({0})'.format("|".join(self.values))) + raise KeyError( + "Invalid value passed. The accepted values are: ({0})".format("|".join(self.values)) + ) return value class Mapping(OneOf): """Ensures the value is one of an acceptable set of values mapping those values to a Python equivelent""" - __slots__ = ('value_map', ) + + __slots__ = ("value_map",) def __init__(self, value_map): self.value_map = value_map @@ -306,16 +349,19 @@ def __init__(self, value_map): @property def __doc__(self): - return 'Accepts one of the following values: ({0})'.format("|".join(self.values)) + return "Accepts one of the following values: ({0})".format("|".join(self.values)) def __call__(self, value): if not value in self.values: - raise KeyError('Invalid value passed. The accepted values are: ({0})'.format("|".join(self.values))) + raise KeyError( + "Invalid value passed. The accepted values are: ({0})".format("|".join(self.values)) + ) return self.value_map[value] class JSON(Type): """Accepts a JSON formatted data structure""" + __slots__ = () def __call__(self, value): @@ -323,29 +369,30 @@ def __call__(self, value): try: return json_converter.loads(value) except Exception: - raise ValueError('Incorrectly formatted JSON provided') + raise ValueError("Incorrectly formatted JSON provided") if type(value) is list: # If Falcon is set to comma-separate entries, this segment joins them again. try: fixed_value = ",".join(value) return json_converter.loads(fixed_value) except Exception: - raise ValueError('Incorrectly formatted JSON provided') + raise ValueError("Incorrectly formatted JSON provided") else: return value class Multi(Type): """Enables accepting one of multiple type methods""" - __slots__ = ('types', ) + + __slots__ = ("types",) def __init__(self, *types): - self.types = types + self.types = types @property def __doc__(self): type_strings = (type_method.__doc__ for type_method in self.types) - return 'Accepts any of the following value types:{0}\n'.format('\n - '.join(type_strings)) + return "Accepts any of the following value types:{0}\n".format("\n - ".join(type_strings)) def __call__(self, value): for type_method in self.types: @@ -358,7 +405,8 @@ def __call__(self, value): class InRange(Type): """Accepts a number within a lower and upper bound of acceptable values""" - __slots__ = ('lower', 'upper', 'convert') + + __slots__ = ("lower", "upper", "convert") def __init__(self, lower, upper, convert=number): self.lower = lower @@ -367,8 +415,9 @@ def __init__(self, lower, upper, convert=number): @property def __doc__(self): - return "{0} that is greater or equal to {1} and less than {2}".format(self.convert.__doc__, - self.lower, self.upper) + return "{0} that is greater or equal to {1} and less than {2}".format( + self.convert.__doc__, self.lower, self.upper + ) def __call__(self, value): value = self.convert(value) @@ -381,7 +430,8 @@ def __call__(self, value): class LessThan(Type): """Accepts a number within a lower and upper bound of acceptable values""" - __slots__ = ('limit', 'convert') + + __slots__ = ("limit", "convert") def __init__(self, limit, convert=number): self.limit = limit @@ -389,7 +439,7 @@ def __init__(self, limit, convert=number): @property def __doc__(self): - return "{0} that is less than {1}".format(self.convert.__doc__, self.limit) + return "{0} that is less than {1}".format(self.convert.__doc__, self.limit) def __call__(self, value): value = self.convert(value) @@ -400,7 +450,8 @@ def __call__(self, value): class GreaterThan(Type): """Accepts a value above a given minimum""" - __slots__ = ('minimum', 'convert') + + __slots__ = ("minimum", "convert") def __init__(self, minimum, convert=number): self.minimum = minimum @@ -419,7 +470,8 @@ def __call__(self, value): class Length(Type): """Accepts a a value that is within a specific length limit""" - __slots__ = ('lower', 'upper', 'convert') + + __slots__ = ("lower", "upper", "convert") def __init__(self, lower, upper, convert=text): self.lower = lower @@ -428,22 +480,28 @@ def __init__(self, lower, upper, convert=text): @property def __doc__(self): - return ("{0} that has a length longer or equal to {1} and less then {2}".format(self.convert.__doc__, - self.lower, self.upper)) + return "{0} that has a length longer or equal to {1} and less then {2}".format( + self.convert.__doc__, self.lower, self.upper + ) def __call__(self, value): value = self.convert(value) length = len(value) if length < self.lower: - raise ValueError("'{0}' is shorter than the lower limit of {1}".format(value, self.lower)) + raise ValueError( + "'{0}' is shorter than the lower limit of {1}".format(value, self.lower) + ) if length >= self.upper: - raise ValueError("'{0}' is longer then the allowed limit of {1}".format(value, self.upper)) + raise ValueError( + "'{0}' is longer then the allowed limit of {1}".format(value, self.upper) + ) return value class ShorterThan(Type): """Accepts a text value shorter than the specified length limit""" - __slots__ = ('limit', 'convert') + + __slots__ = ("limit", "convert") def __init__(self, limit, convert=text): self.limit = limit @@ -457,13 +515,16 @@ def __call__(self, value): value = self.convert(value) length = len(value) if not length < self.limit: - raise ValueError("'{0}' is longer then the allowed limit of {1}".format(value, self.limit)) + raise ValueError( + "'{0}' is longer then the allowed limit of {1}".format(value, self.limit) + ) return value class LongerThan(Type): """Accepts a value up to the specified limit""" - __slots__ = ('limit', 'convert') + + __slots__ = ("limit", "convert") def __init__(self, limit, convert=text): self.limit = limit @@ -483,7 +544,8 @@ def __call__(self, value): class CutOff(Type): """Cuts off the provided value at the specified index""" - __slots__ = ('limit', 'convert') + + __slots__ = ("limit", "convert") def __init__(self, limit, convert=text): self.limit = limit @@ -491,15 +553,18 @@ def __init__(self, limit, convert=text): @property def __doc__(self): - return "'{0}' with anything over the length of {1} being ignored".format(self.convert.__doc__, self.limit) + return "'{0}' with anything over the length of {1} being ignored".format( + self.convert.__doc__, self.limit + ) def __call__(self, value): - return self.convert(value)[:self.limit] + return self.convert(value)[: self.limit] class Chain(Type): """type for chaining multiple types together""" - __slots__ = ('types', ) + + __slots__ = ("types",) def __init__(self, *types): self.types = types @@ -512,7 +577,8 @@ def __call__(self, value): class Nullable(Chain): """A Chain types that Allows None values""" - __slots__ = ('types', ) + + __slots__ = ("types",) def __init__(self, *types): self.types = types @@ -526,7 +592,8 @@ def __call__(self, value): class TypedProperty(object): """class for building property objects for schema objects""" - __slots__ = ('name', 'type_func') + + __slots__ = ("name", "type_func") def __init__(self, name, type_func): self.name = "_" + name @@ -544,10 +611,15 @@ def __delete__(self, instance): class NewTypeMeta(type): """Meta class to turn Schema objects into format usable by hug""" + __slots__ = () def __init__(cls, name, bases, nmspc): - cls._types = {attr: getattr(cls, attr) for attr in dir(cls) if getattr(getattr(cls, attr), "_hug_type", False)} + cls._types = { + attr: getattr(cls, attr) + for attr in dir(cls) + if getattr(getattr(cls, attr), "_hug_type", False) + } slots = getattr(cls, "__slots__", ()) slots = set(slots) for attr, type_func in cls._types.items(): @@ -561,6 +633,7 @@ def __init__(cls, name, bases, nmspc): class Schema(object, metaclass=NewTypeMeta): """Schema for creating complex types using hug types""" + __slots__ = () def __new__(cls, json, *args, **kwargs): @@ -576,12 +649,14 @@ def __init__(self, json, force=False): key = "_" + key setattr(self, key, value) + json = JSON() class MarshmallowInputSchema(Type): """Allows using a Marshmallow Schema directly in a hug input type annotation""" - __slots__ = ("schema") + + __slots__ = "schema" def __init__(self, schema): self.schema = schema @@ -597,22 +672,29 @@ def __call__(self, value, context): # In marshmallow 3 schemas always raise Validation error on load if input data is invalid and a single # value `data` is returned. if MARSHMALLOW_MAJOR_VERSION is None or MARSHMALLOW_MAJOR_VERSION == 2: - value, errors = self.schema.loads(value) if isinstance(value, str) else self.schema.load(value) + value, errors = ( + self.schema.loads(value) if isinstance(value, str) else self.schema.load(value) + ) else: errors = {} try: - value = self.schema.loads(value) if isinstance(value, str) else self.schema.load(value) + value = ( + self.schema.loads(value) if isinstance(value, str) else self.schema.load(value) + ) except ValidationError as e: errors = e.messages if errors: - raise InvalidTypeData('Invalid {0} passed in'.format(self.schema.__class__.__name__), errors) + raise InvalidTypeData( + "Invalid {0} passed in".format(self.schema.__class__.__name__), errors + ) return value class MarshmallowReturnSchema(Type): """Allows using a Marshmallow Schema directly in a hug return type annotation""" - __slots__ = ("schema", ) + + __slots__ = ("schema",) def __init__(self, schema): self.schema = schema @@ -644,11 +726,12 @@ def __call__(self, value): errors = e.messages if errors: - raise InvalidTypeData('Invalid {0} passed in'.format(self.schema.__class__.__name__), errors) + raise InvalidTypeData( + "Invalid {0} passed in".format(self.schema.__class__.__name__), errors + ) return value - multiple = Multiple() smart_boolean = SmartBoolean() inline_dictionary = InlineDictionary() diff --git a/hug/use.py b/hug/use.py index b1c8de2e..a6265e34 100644 --- a/hug/use.py +++ b/hug/use.py @@ -35,67 +35,79 @@ from hug.defaults import input_format from hug.format import parse_content_type -Response = namedtuple('Response', ('data', 'status_code', 'headers')) -Request = namedtuple('Request', ('content_length', 'stream', 'params')) +Response = namedtuple("Response", ("data", "status_code", "headers")) +Request = namedtuple("Request", ("content_length", "stream", "params")) class Service(object): """Defines the base concept of a consumed service. This is to enable encapsulating the logic of calling a service so usage can be independant of the interface """ - __slots__ = ('timeout', 'raise_on', 'version') - def __init__(self, version=None, timeout=None, raise_on=(500, ), **kwargs): + __slots__ = ("timeout", "raise_on", "version") + + def __init__(self, version=None, timeout=None, raise_on=(500,), **kwargs): self.version = version self.timeout = timeout - self.raise_on = raise_on if type(raise_on) in (tuple, list) else (raise_on, ) + self.raise_on = raise_on if type(raise_on) in (tuple, list) else (raise_on,) - def request(self, method, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): + def request( + self, method, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params + ): """Calls the service at the specified URL using the "CALL" method""" raise NotImplementedError("Concrete services must define the request method") def get(self, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): """Calls the service at the specified URL using the "GET" method""" - return self.request('GET', url=url, headers=headers, timeout=timeout, **params) + return self.request("GET", url=url, headers=headers, timeout=timeout, **params) def post(self, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): """Calls the service at the specified URL using the "POST" method""" - return self.request('POST', url=url, headers=headers, timeout=timeout, **params) + return self.request("POST", url=url, headers=headers, timeout=timeout, **params) def delete(self, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): """Calls the service at the specified URL using the "DELETE" method""" - return self.request('DELETE', url=url, headers=headers, timeout=timeout, **params) + return self.request("DELETE", url=url, headers=headers, timeout=timeout, **params) def put(self, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): """Calls the service at the specified URL using the "PUT" method""" - return self.request('PUT', url=url, headers=headers, timeout=timeout, **params) + return self.request("PUT", url=url, headers=headers, timeout=timeout, **params) def trace(self, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): """Calls the service at the specified URL using the "TRACE" method""" - return self.request('TRACE', url=url, headers=headers, timeout=timeout, **params) + return self.request("TRACE", url=url, headers=headers, timeout=timeout, **params) def patch(self, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): """Calls the service at the specified URL using the "PATCH" method""" - return self.request('PATCH', url=url, headers=headers, timeout=timeout, **params) + return self.request("PATCH", url=url, headers=headers, timeout=timeout, **params) def options(self, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): """Calls the service at the specified URL using the "OPTIONS" method""" - return self.request('OPTIONS', url=url, headers=headers, timeout=timeout, **params) + return self.request("OPTIONS", url=url, headers=headers, timeout=timeout, **params) def head(self, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): """Calls the service at the specified URL using the "HEAD" method""" - return self.request('HEAD', url=url, headers=headers, timeout=timeout, **params) + return self.request("HEAD", url=url, headers=headers, timeout=timeout, **params) def connect(self, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): """Calls the service at the specified URL using the "CONNECT" method""" - return self.request('CONNECT', url=url, headers=headers, timeout=timeout, **params) + return self.request("CONNECT", url=url, headers=headers, timeout=timeout, **params) class HTTP(Service): - __slots__ = ('endpoint', 'session', 'json_transport') - - def __init__(self, endpoint, auth=None, version=None, headers=empty.dict, timeout=None, raise_on=(500, ), - json_transport=True, **kwargs): + __slots__ = ("endpoint", "session", "json_transport") + + def __init__( + self, + endpoint, + auth=None, + version=None, + headers=empty.dict, + timeout=None, + raise_on=(500,), + json_transport=True, + **kwargs + ): super().__init__(timeout=timeout, raise_on=raise_on, version=version, **kwargs) self.endpoint = endpoint self.session = requests.Session() @@ -103,48 +115,62 @@ def __init__(self, endpoint, auth=None, version=None, headers=empty.dict, timeou self.session.headers.update(headers) self.json_transport = json_transport - def request(self, method, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): - url = "{0}/{1}".format(self.version, url.lstrip('/')) if self.version else url - kwargs = {'json' if self.json_transport else 'params': params} - response = self.session.request(method, self.endpoint + url.format(url_params), headers=headers, **kwargs) + def request( + self, method, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params + ): + url = "{0}/{1}".format(self.version, url.lstrip("/")) if self.version else url + kwargs = {"json" if self.json_transport else "params": params} + response = self.session.request( + method, self.endpoint + url.format(url_params), headers=headers, **kwargs + ) data = BytesIO(response.content) - content_type, content_params = parse_content_type(response.headers.get('content-type', '')) + content_type, content_params = parse_content_type(response.headers.get("content-type", "")) if content_type in input_format: data = input_format[content_type](data, **content_params) if response.status_code in self.raise_on: - raise requests.HTTPError('{0} {1} occured for url: {2}'.format(response.status_code, response.reason, url)) + raise requests.HTTPError( + "{0} {1} occured for url: {2}".format(response.status_code, response.reason, url) + ) return Response(data, response.status_code, response.headers) class Local(Service): - __slots__ = ('api', 'headers') + __slots__ = ("api", "headers") - def __init__(self, api, version=None, headers=empty.dict, timeout=None, raise_on=(500, ), **kwargs): + def __init__( + self, api, version=None, headers=empty.dict, timeout=None, raise_on=(500,), **kwargs + ): super().__init__(timeout=timeout, raise_on=raise_on, version=version, **kwargs) self.api = API(api) self.headers = headers - def request(self, method, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params): + def request( + self, method, url, url_params=empty.dict, headers=empty.dict, timeout=None, **params + ): function = self.api.http.versioned.get(self.version, {}).get(url, None) if not function: function = self.api.http.versioned.get(None, {}).get(url, None) if not function: if 404 in self.raise_on: - raise requests.HTTPError('404 Not Found occured for url: {0}'.format(url)) - return Response('Not Found', 404, {'content-type', 'application/json'}) + raise requests.HTTPError("404 Not Found occured for url: {0}".format(url)) + return Response("Not Found", 404, {"content-type", "application/json"}) interface = function.interface.http response = falcon.Response() request = Request(None, None, empty.dict) - context = self.api.context_factory(api=self.api, api_version=self.version, interface=interface) + context = self.api.context_factory( + api=self.api, api_version=self.version, interface=interface + ) interface.set_response_defaults(response) params.update(url_params) - params = interface.gather_parameters(request, response, context, api_version=self.version, **params) + params = interface.gather_parameters( + request, response, context, api_version=self.version, **params + ) errors = interface.validate(params, context) if errors: interface.render_errors(errors, request, response) @@ -152,42 +178,53 @@ def request(self, method, url, url_params=empty.dict, headers=empty.dict, timeou interface.render_content(interface.call_function(params), context, request, response) data = BytesIO(response.data) - content_type, content_params = parse_content_type(response._headers.get('content-type', '')) + content_type, content_params = parse_content_type(response._headers.get("content-type", "")) if content_type in input_format: data = input_format[content_type](data, **content_params) - status_code = int(''.join(re.findall('\d+', response.status))) + status_code = int("".join(re.findall("\d+", response.status))) if status_code in self.raise_on: - raise requests.HTTPError('{0} occured for url: {1}'.format(response.status, url)) + raise requests.HTTPError("{0} occured for url: {1}".format(response.status, url)) return Response(data, status_code, response._headers) class Socket(Service): - __slots__ = ('connection_pool', 'timeout', 'connection', 'send_and_receive') + __slots__ = ("connection_pool", "timeout", "connection", "send_and_receive") - on_unix = getattr(socket, 'AF_UNIX', False) - Connection = namedtuple('Connection', ('connect_to', 'proto', 'sockopts')) + on_unix = getattr(socket, "AF_UNIX", False) + Connection = namedtuple("Connection", ("connect_to", "proto", "sockopts")) protocols = { - 'tcp': (socket.AF_INET, socket.SOCK_STREAM), - 'udp': (socket.AF_INET, socket.SOCK_DGRAM), + "tcp": (socket.AF_INET, socket.SOCK_STREAM), + "udp": (socket.AF_INET, socket.SOCK_DGRAM), } - streams = set(('tcp',)) - datagrams = set(('udp',)) - inet = set(('tcp', 'udp',)) + streams = set(("tcp",)) + datagrams = set(("udp",)) + inet = set(("tcp", "udp")) unix = set() if on_unix: - protocols.update({ - 'unix_dgram': (socket.AF_UNIX, socket.SOCK_DGRAM), - 'unix_stream': (socket.AF_UNIX, socket.SOCK_STREAM) - }) - streams.add('unix_stream') - datagrams.add('unix_dgram') - unix.update(('unix_stream', 'unix_dgram')) - - def __init__(self, connect_to, proto, version=None, - headers=empty.dict, timeout=None, pool=0, raise_on=(500, ), **kwargs): + protocols.update( + { + "unix_dgram": (socket.AF_UNIX, socket.SOCK_DGRAM), + "unix_stream": (socket.AF_UNIX, socket.SOCK_STREAM), + } + ) + streams.add("unix_stream") + datagrams.add("unix_dgram") + unix.update(("unix_stream", "unix_dgram")) + + def __init__( + self, + connect_to, + proto, + version=None, + headers=empty.dict, + timeout=None, + pool=0, + raise_on=(500,), + **kwargs + ): super().__init__(timeout=timeout, raise_on=raise_on, version=version, **kwargs) connect_to = tuple(connect_to) if proto in Socket.inet else connect_to self.timeout = timeout @@ -231,8 +268,8 @@ def _stream_send_and_receive(self, _socket, message, *args, **kwargs): """TCP/Stream sender and receiver""" data = BytesIO() - _socket_fd = _socket.makefile(mode='rwb', encoding='utf-8') - _socket_fd.write(message.encode('utf-8')) + _socket_fd = _socket.makefile(mode="rwb", encoding="utf-8") + _socket_fd.write(message.encode("utf-8")) _socket_fd.flush() for received in _socket_fd: @@ -244,7 +281,7 @@ def _stream_send_and_receive(self, _socket, message, *args, **kwargs): def _dgram_send_and_receive(self, _socket, message, buffer_size=4096, *args): """User Datagram Protocol sender and receiver""" - _socket.send(message.encode('utf-8')) + _socket.send(message.encode("utf-8")) data, address = _socket.recvfrom(buffer_size) return BytesIO(data) diff --git a/hug/validate.py b/hug/validate.py index b61d70d1..82010074 100644 --- a/hug/validate.py +++ b/hug/validate.py @@ -24,6 +24,7 @@ def all(*validators): """Validation only succeeds if all passed in validators return no errors""" + def validate_all(fields): for validator in validators: errors = validator(fields) @@ -36,6 +37,7 @@ def validate_all(fields): def any(*validators): """If any of the specified validators pass the validation succeeds""" + def validate_any(fields): errors = {} for validator in validators: @@ -51,7 +53,7 @@ def validate_any(fields): def contains_one_of(*fields): """Enables ensuring that one of multiple optional fields is set""" - message = 'Must contain any one of the following fields: {0}'.format(', '.join(fields)) + message = "Must contain any one of the following fields: {0}".format(", ".join(fields)) def check_contains(endpoint_fields): for field in fields: @@ -60,7 +62,8 @@ def check_contains(endpoint_fields): errors = {} for field in fields: - errors[field] = 'one of these must have a value' + errors[field] = "one of these must have a value" return errors + check_contains.__doc__ = message return check_contains diff --git a/setup.py b/setup.py index 85181e2b..c7ccafd4 100755 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ MYDIR = path.abspath(os.path.dirname(__file__)) CYTHON = False -JYTHON = 'java' in sys.platform +JYTHON = "java" in sys.platform ext_modules = [] cmdclass = {} @@ -41,81 +41,80 @@ PYPY = False if not PYPY and not JYTHON: - if '--without-cython' in sys.argv: - sys.argv.remove('--without-cython') + if "--without-cython" in sys.argv: + sys.argv.remove("--without-cython") CYTHON = False else: try: from Cython.Distutils import build_ext + CYTHON = True except ImportError: CYTHON = False if CYTHON: + def list_modules(dirname): - filenames = glob.glob(path.join(dirname, '*.py')) + filenames = glob.glob(path.join(dirname, "*.py")) module_names = [] for name in filenames: module, ext = path.splitext(path.basename(name)) - if module != '__init__': + if module != "__init__": module_names.append(module) return module_names ext_modules = [ - Extension('hug.' + ext, [path.join('hug', ext + '.py')]) - for ext in list_modules(path.join(MYDIR, 'hug'))] - cmdclass['build_ext'] = build_ext + Extension("hug." + ext, [path.join("hug", ext + ".py")]) + for ext in list_modules(path.join(MYDIR, "hug")) + ] + cmdclass["build_ext"] = build_ext -with open('README.md', encoding='utf-8') as f: # Loads in the README for PyPI +with open("README.md", encoding="utf-8") as f: # Loads in the README for PyPI long_description = f.read() setup( - name='hug', - version='2.5.0', - description='A Python framework that makes developing APIs ' - 'as simple as possible, but no simpler.', + name="hug", + version="2.5.0", + description="A Python framework that makes developing APIs " + "as simple as possible, but no simpler.", long_description=long_description, # PEP 566, the new PyPI, and setuptools>=38.6.0 make markdown possible - long_description_content_type='text/markdown', - author='Timothy Crosley', - author_email='timothy.crosley@gmail.com', + long_description_content_type="text/markdown", + author="Timothy Crosley", + author_email="timothy.crosley@gmail.com", # These appear in the left hand side bar on PyPI - url='https://github.com/timothycrosley/hug', + url="https://github.com/timothycrosley/hug", project_urls={ - 'Documentation': 'http://www.hug.rest/', - 'Gitter': 'https://gitter.im/timothycrosley/hug', + "Documentation": "http://www.hug.rest/", + "Gitter": "https://gitter.im/timothycrosley/hug", }, license="MIT", - entry_points={ - 'console_scripts': [ - 'hug = hug:development_runner.hug.interface.cli', - ] - }, - packages=['hug'], - requires=['falcon', 'requests'], - install_requires=['falcon==2.0.0', 'requests'], - setup_requires=['pytest-runner'], - tests_require=['pytest', 'mock', 'marshmallow'], + entry_points={"console_scripts": ["hug = hug:development_runner.hug.interface.cli"]}, + packages=["hug"], + requires=["falcon", "requests"], + install_requires=["falcon==2.0.0", "requests"], + setup_requires=["pytest-runner"], + tests_require=["pytest", "mock", "marshmallow"], ext_modules=ext_modules, cmdclass=cmdclass, python_requires=">=3.5", - keywords='Web, Python, Python3, Refactoring, REST, Framework, RPC', + keywords="Web, Python, Python3, Refactoring, REST, Framework, RPC", classifiers=[ - 'Development Status :: 6 - Mature', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Environment :: Console', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Topic :: Software Development :: Libraries', - 'Topic :: Utilities' - ] + "Development Status :: 6 - Mature", + "Intended Audience :: Developers", + "Natural Language :: English", + "Environment :: Console", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", + ], ) diff --git a/tests/constants.py b/tests/constants.py index 75d8ab63..9f175da7 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -23,4 +23,4 @@ import os TEST_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) -BASE_DIRECTORY = os.path.realpath(os.path.join(TEST_DIRECTORY, '..')) +BASE_DIRECTORY = os.path.realpath(os.path.join(TEST_DIRECTORY, "..")) diff --git a/tests/fixtures.py b/tests/fixtures.py index b142c69d..07470c01 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -6,7 +6,7 @@ import hug -Routers = namedtuple('Routers', ['http', 'local', 'cli']) +Routers = namedtuple("Routers", ["http", "local", "cli"]) class TestAPI(hug.API): @@ -16,10 +16,12 @@ class TestAPI(hug.API): @pytest.fixture def hug_api(): """Defines a dependency for and then includes a uniquely identified hug API for a single test case""" - api = TestAPI('fake_api_{}'.format(randint(0, 1000000))) - api.route = Routers(hug.routing.URLRouter().api(api), - hug.routing.LocalRouter().api(api), - hug.routing.CLIRouter().api(api)) + api = TestAPI("fake_api_{}".format(randint(0, 1000000))) + api.route = Routers( + hug.routing.URLRouter().api(api), + hug.routing.LocalRouter().api(api), + hug.routing.CLIRouter().api(api), + ) return api @@ -29,4 +31,4 @@ def hug_api_error_exit_codes_enabled(): Defines a dependency for and then includes a uniquely identified hug API for a single test case with error exit codes enabled. """ - return TestAPI('fake_api_{}'.format(randint(0, 1000000)), cli_error_exit_codes=True) + return TestAPI("fake_api_{}".format(randint(0, 1000000)), cli_error_exit_codes=True) diff --git a/tests/module_fake.py b/tests/module_fake.py index f1fb2fc2..328feaca 100644 --- a/tests/module_fake.py +++ b/tests/module_fake.py @@ -12,7 +12,7 @@ def my_directive(default=None, **kwargs): return default -@hug.default_input_format('application/made-up') +@hug.default_input_format("application/made-up") def made_up_formatter(data): """for testing""" return data @@ -36,7 +36,7 @@ def my_directive_global(default=None, **kwargs): return default -@hug.default_input_format('application/made-up', apply_globally=True) +@hug.default_input_format("application/made-up", apply_globally=True) def made_up_formatter_global(data): """for testing""" return data @@ -63,10 +63,10 @@ def on_startup(api): @hug.static() def static(): """for testing""" - return ('', ) + return ("",) -@hug.sink('/all') +@hug.sink("/all") def sink(path): """for testing""" return path diff --git a/tests/module_fake_http_and_cli.py b/tests/module_fake_http_and_cli.py index 783b1cc8..2eda4c37 100644 --- a/tests/module_fake_http_and_cli.py +++ b/tests/module_fake_http_and_cli.py @@ -4,4 +4,4 @@ @hug.get() @hug.cli() def made_up_go(): - return 'Going!' + return "Going!" diff --git a/tests/module_fake_many_methods.py b/tests/module_fake_many_methods.py index febb7134..388c2801 100644 --- a/tests/module_fake_many_methods.py +++ b/tests/module_fake_many_methods.py @@ -5,10 +5,10 @@ @hug.get() def made_up_hello(): """GETting for science!""" - return 'hello from GET' + return "hello from GET" @hug.post() def made_up_hello(): """POSTing for science!""" - return 'hello from POST' + return "hello from POST" diff --git a/tests/module_fake_post.py b/tests/module_fake_post.py index d1dd7212..5752fea3 100644 --- a/tests/module_fake_post.py +++ b/tests/module_fake_post.py @@ -5,4 +5,4 @@ @hug.post() def made_up_hello(): """POSTing for science!""" - return 'hello from POST' + return "hello from POST" diff --git a/tests/module_fake_simple.py b/tests/module_fake_simple.py index 85b2689b..362b1fe6 100644 --- a/tests/module_fake_simple.py +++ b/tests/module_fake_simple.py @@ -5,11 +5,13 @@ class FakeSimpleException(Exception): pass + @hug.get() def made_up_hello(): """for science!""" - return 'hello' + return "hello" + -@hug.get('/exception') +@hug.get("/exception") def made_up_exception(): - raise FakeSimpleException('test') + raise FakeSimpleException("test") diff --git a/tests/test_api.py b/tests/test_api.py index 885a33ef..16dbaa5b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -35,15 +35,16 @@ def test_singleton(self): def test_context(self): """Test to ensure the hug singleton provides a global modifiable context""" - assert not hasattr(hug.API(__name__), '_context') + assert not hasattr(hug.API(__name__), "_context") assert hug.API(__name__).context == {} - assert hasattr(hug.API(__name__), '_context') + assert hasattr(hug.API(__name__), "_context") def test_dynamic(self): """Test to ensure it's possible to dynamically create new modules to house APIs based on name alone""" - new_api = hug.API('module_created_on_the_fly') - assert new_api.module.__name__ == 'module_created_on_the_fly' + new_api = hug.API("module_created_on_the_fly") + assert new_api.module.__name__ == "module_created_on_the_fly" import module_created_on_the_fly + assert module_created_on_the_fly assert module_created_on_the_fly.__hug__ == new_api @@ -63,14 +64,14 @@ def test_anonymous(): """Ensure it's possible to create anonymous APIs""" assert hug.API() != hug.API() != api assert hug.API().module == None - assert hug.API().name == '' - assert hug.API(name='my_name').name == 'my_name' - assert hug.API(doc='Custom documentation').doc == 'Custom documentation' + assert hug.API().name == "" + assert hug.API(name="my_name").name == "my_name" + assert hug.API(doc="Custom documentation").doc == "Custom documentation" def test_api_routes(hug_api): """Ensure http API can return a quick mapping all urls to method""" - hug_api.http.base_url = '/root' + hug_api.http.base_url = "/root" @hug.get(api=hug_api) def my_route(): @@ -84,10 +85,16 @@ def my_second_route(): def my_cli_command(): pass - assert list(hug_api.http.urls()) == ['/root/my_route', '/root/my_second_route'] - assert list(hug_api.http.handlers()) == [my_route.interface.http, my_second_route.interface.http] - assert list(hug_api.handlers()) == [my_route.interface.http, my_second_route.interface.http, - my_cli_command.interface.cli] + assert list(hug_api.http.urls()) == ["/root/my_route", "/root/my_second_route"] + assert list(hug_api.http.handlers()) == [ + my_route.interface.http, + my_second_route.interface.http, + ] + assert list(hug_api.handlers()) == [ + my_route.interface.http, + my_second_route.interface.http, + my_cli_command.interface.cli, + ] def test_cli_interface_api_with_exit_codes(hug_api_error_exit_codes_enabled): @@ -103,10 +110,10 @@ def true(self): def false(self): return False - api.cli(args=[None, 'true']) + api.cli(args=[None, "true"]) with pytest.raises(SystemExit): - api.cli(args=[None, 'false']) + api.cli(args=[None, "false"]) def test_cli_interface_api_without_exit_codes(): @@ -120,5 +127,5 @@ def true(self): def false(self): return False - api.cli(args=[None, 'true']) - api.cli(args=[None, 'false']) + api.cli(args=[None, "true"]) + api.cli(args=[None, "false"]) diff --git a/tests/test_async.py b/tests/test_async.py index 4cb9f9aa..f25945be 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -30,6 +30,7 @@ def test_basic_call_async(): """ The most basic Happy-Path test for Hug APIs using async """ + @hug.call() async def hello_world(): return "Hello World!" @@ -39,6 +40,7 @@ async def hello_world(): def tested_nested_basic_call_async(): """Test to ensure the most basic call still works if applied to a method""" + @hug.call() async def hello_world(self=None): return await nested_hello_world() @@ -49,13 +51,13 @@ async def nested_hello_world(self=None): assert hello_world.interface.http assert loop.run_until_complete(hello_world()) == "Hello World!" - assert hug.test.get(api, '/hello_world').data == "Hello World!" + assert hug.test.get(api, "/hello_world").data == "Hello World!" def test_basic_call_on_method_async(): """Test to ensure the most basic call still works if applied to a method""" - class API(object): + class API(object): @hug.call() async def hello_world(self=None): return "Hello World!" @@ -63,13 +65,13 @@ async def hello_world(self=None): api_instance = API() assert api_instance.hello_world.interface.http assert loop.run_until_complete(api_instance.hello_world()) == "Hello World!" - assert hug.test.get(api, '/hello_world').data == "Hello World!" + assert hug.test.get(api, "/hello_world").data == "Hello World!" def test_basic_call_on_method_through_api_instance_async(): """Test to ensure instance method calling via async works as expected""" - class API(object): + class API(object): def hello_world(self): return "Hello World!" @@ -80,13 +82,13 @@ async def hello_world(): return api_instance.hello_world() assert api_instance.hello_world() == "Hello World!" - assert hug.test.get(api, '/hello_world').data == "Hello World!" + assert hug.test.get(api, "/hello_world").data == "Hello World!" def test_basic_call_on_method_registering_without_decorator_async(): """Test to ensure async methods can be used without decorator""" - class API(object): + class API(object): def __init__(self): hug.call()(self.hello_world_method) @@ -96,4 +98,4 @@ async def hello_world_method(self): api_instance = API() assert loop.run_until_complete(api_instance.hello_world_method()) == "Hello World!" - assert hug.test.get(api, '/hello_world_method').data == "Hello World!" + assert hug.test.get(api, "/hello_world_method").data == "Hello World!" diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 82ab00a7..2beb22cf 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -31,25 +31,38 @@ def test_basic_auth(): """Test to ensure hug provides basic_auth handler works as expected""" - @hug.get(requires=hug.authentication.basic(hug.authentication.verify('Tim', 'Custom password'))) + @hug.get(requires=hug.authentication.basic(hug.authentication.verify("Tim", "Custom password"))) def hello_world(): - return 'Hello world!' - - assert '401' in hug.test.get(api, 'hello_world').status - assert '401' in hug.test.get(api, 'hello_world', headers={'Authorization': 'Not correctly formed'}).status - assert '401' in hug.test.get(api, 'hello_world', headers={'Authorization': 'Nospaces'}).status - assert '401' in hug.test.get(api, 'hello_world', headers={'Authorization': 'Basic VXNlcjE6bXlwYXNzd29yZA'}).status - - token = b64encode('{0}:{1}'.format('Tim', 'Custom password').encode('utf8')).decode('utf8') - assert hug.test.get(api, 'hello_world', headers={'Authorization': 'Basic {0}'.format(token)}).data == 'Hello world!' - - token = b'Basic ' + b64encode('{0}:{1}'.format('Tim', 'Custom password').encode('utf8')) - assert hug.test.get(api, 'hello_world', headers={'Authorization': token}).data == 'Hello world!' - - token = b'Basic ' + b64encode('{0}:{1}'.format('Tim', 'Wrong password').encode('utf8')) - assert '401' in hug.test.get(api, 'hello_world', headers={'Authorization': token}).status - - custom_context = dict(custom='context', username='Tim', password='Custom password') + return "Hello world!" + + assert "401" in hug.test.get(api, "hello_world").status + assert ( + "401" + in hug.test.get( + api, "hello_world", headers={"Authorization": "Not correctly formed"} + ).status + ) + assert "401" in hug.test.get(api, "hello_world", headers={"Authorization": "Nospaces"}).status + assert ( + "401" + in hug.test.get( + api, "hello_world", headers={"Authorization": "Basic VXNlcjE6bXlwYXNzd29yZA"} + ).status + ) + + token = b64encode("{0}:{1}".format("Tim", "Custom password").encode("utf8")).decode("utf8") + assert ( + hug.test.get(api, "hello_world", headers={"Authorization": "Basic {0}".format(token)}).data + == "Hello world!" + ) + + token = b"Basic " + b64encode("{0}:{1}".format("Tim", "Custom password").encode("utf8")) + assert hug.test.get(api, "hello_world", headers={"Authorization": token}).data == "Hello world!" + + token = b"Basic " + b64encode("{0}:{1}".format("Tim", "Wrong password").encode("utf8")) + assert "401" in hug.test.get(api, "hello_world", headers={"Authorization": token}).status + + custom_context = dict(custom="context", username="Tim", password="Custom password") @hug.context_factory() def create_test_context(*args, **kwargs): @@ -59,43 +72,58 @@ def create_test_context(*args, **kwargs): def delete_custom_context(context, exception=None, errors=None, lacks_requirement=None): assert context == custom_context assert not errors - context['exception'] = exception + context["exception"] = exception @hug.authentication.basic def context_basic_authentication(username, password, context): assert context == custom_context - if username == context['username'] and password == context['password']: + if username == context["username"] and password == context["password"]: return True @hug.get(requires=context_basic_authentication) def hello_context(): - return 'context!' - - assert '401' in hug.test.get(api, 'hello_context').status - assert isinstance(custom_context['exception'], HTTPUnauthorized) - del custom_context['exception'] - assert '401' in hug.test.get(api, 'hello_context', headers={'Authorization': 'Not correctly formed'}).status - assert isinstance(custom_context['exception'], HTTPUnauthorized) - del custom_context['exception'] - assert '401' in hug.test.get(api, 'hello_context', headers={'Authorization': 'Nospaces'}).status - assert isinstance(custom_context['exception'], HTTPUnauthorized) - del custom_context['exception'] - assert '401' in hug.test.get(api, 'hello_context', headers={'Authorization': 'Basic VXNlcjE6bXlwYXNzd29yZA'}).status - assert isinstance(custom_context['exception'], HTTPUnauthorized) - del custom_context['exception'] - - token = b64encode('{0}:{1}'.format('Tim', 'Custom password').encode('utf8')).decode('utf8') - assert hug.test.get(api, 'hello_context', headers={'Authorization': 'Basic {0}'.format(token)}).data == 'context!' - assert not custom_context['exception'] - del custom_context['exception'] - token = b'Basic ' + b64encode('{0}:{1}'.format('Tim', 'Custom password').encode('utf8')) - assert hug.test.get(api, 'hello_context', headers={'Authorization': token}).data == 'context!' - assert not custom_context['exception'] - del custom_context['exception'] - token = b'Basic ' + b64encode('{0}:{1}'.format('Tim', 'Wrong password').encode('utf8')) - assert '401' in hug.test.get(api, 'hello_context', headers={'Authorization': token}).status - assert isinstance(custom_context['exception'], HTTPUnauthorized) - del custom_context['exception'] + return "context!" + + assert "401" in hug.test.get(api, "hello_context").status + assert isinstance(custom_context["exception"], HTTPUnauthorized) + del custom_context["exception"] + assert ( + "401" + in hug.test.get( + api, "hello_context", headers={"Authorization": "Not correctly formed"} + ).status + ) + assert isinstance(custom_context["exception"], HTTPUnauthorized) + del custom_context["exception"] + assert "401" in hug.test.get(api, "hello_context", headers={"Authorization": "Nospaces"}).status + assert isinstance(custom_context["exception"], HTTPUnauthorized) + del custom_context["exception"] + assert ( + "401" + in hug.test.get( + api, "hello_context", headers={"Authorization": "Basic VXNlcjE6bXlwYXNzd29yZA"} + ).status + ) + assert isinstance(custom_context["exception"], HTTPUnauthorized) + del custom_context["exception"] + + token = b64encode("{0}:{1}".format("Tim", "Custom password").encode("utf8")).decode("utf8") + assert ( + hug.test.get( + api, "hello_context", headers={"Authorization": "Basic {0}".format(token)} + ).data + == "context!" + ) + assert not custom_context["exception"] + del custom_context["exception"] + token = b"Basic " + b64encode("{0}:{1}".format("Tim", "Custom password").encode("utf8")) + assert hug.test.get(api, "hello_context", headers={"Authorization": token}).data == "context!" + assert not custom_context["exception"] + del custom_context["exception"] + token = b"Basic " + b64encode("{0}:{1}".format("Tim", "Wrong password").encode("utf8")) + assert "401" in hug.test.get(api, "hello_context", headers={"Authorization": token}).status + assert isinstance(custom_context["exception"], HTTPUnauthorized) + del custom_context["exception"] def test_api_key(): @@ -103,18 +131,18 @@ def test_api_key(): @hug.authentication.api_key def api_key_authentication(api_key): - if api_key == 'Bacon': - return 'Timothy' + if api_key == "Bacon": + return "Timothy" @hug.get(requires=api_key_authentication) def hello_world(): - return 'Hello world!' + return "Hello world!" - assert hug.test.get(api, 'hello_world', headers={'X-Api-Key': 'Bacon'}).data == 'Hello world!' - assert '401' in hug.test.get(api, 'hello_world').status - assert '401' in hug.test.get(api, 'hello_world', headers={'X-Api-Key': 'Invalid'}).status + assert hug.test.get(api, "hello_world", headers={"X-Api-Key": "Bacon"}).data == "Hello world!" + assert "401" in hug.test.get(api, "hello_world").status + assert "401" in hug.test.get(api, "hello_world", headers={"X-Api-Key": "Invalid"}).status - custom_context = dict(custom='context') + custom_context = dict(custom="context") @hug.context_factory() def create_test_context(*args, **kwargs): @@ -124,49 +152,59 @@ def create_test_context(*args, **kwargs): def delete_custom_context(context, exception=None, errors=None, lacks_requirement=None): assert context == custom_context assert not errors - context['exception'] = exception + context["exception"] = exception @hug.authentication.api_key def context_api_key_authentication(api_key, context): assert context == custom_context - if api_key == 'Bacon': - return 'Timothy' + if api_key == "Bacon": + return "Timothy" @hug.get(requires=context_api_key_authentication) def hello_context_world(): - return 'Hello context world!' - - assert hug.test.get(api, 'hello_context_world', headers={'X-Api-Key': 'Bacon'}).data == 'Hello context world!' - assert not custom_context['exception'] - del custom_context['exception'] - assert '401' in hug.test.get(api, 'hello_context_world').status - assert isinstance(custom_context['exception'], HTTPUnauthorized) - del custom_context['exception'] - assert '401' in hug.test.get(api, 'hello_context_world', headers={'X-Api-Key': 'Invalid'}).status - assert isinstance(custom_context['exception'], HTTPUnauthorized) - del custom_context['exception'] + return "Hello context world!" + + assert ( + hug.test.get(api, "hello_context_world", headers={"X-Api-Key": "Bacon"}).data + == "Hello context world!" + ) + assert not custom_context["exception"] + del custom_context["exception"] + assert "401" in hug.test.get(api, "hello_context_world").status + assert isinstance(custom_context["exception"], HTTPUnauthorized) + del custom_context["exception"] + assert ( + "401" in hug.test.get(api, "hello_context_world", headers={"X-Api-Key": "Invalid"}).status + ) + assert isinstance(custom_context["exception"], HTTPUnauthorized) + del custom_context["exception"] def test_token_auth(): """Test JSON Web Token""" - #generated with jwt.encode({'user': 'Timothy','data':'my data'}, 'super-secret-key-please-change', algorithm='HS256') - precomptoken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoibXkgZGF0YSIsInVzZXIiOiJUaW1vdGh5In0.' \ - '8QqzQMJUTq0Dq7vHlnDjdoCKFPDAlvxGCpc_8XF41nI' + # generated with jwt.encode({'user': 'Timothy','data':'my data'}, 'super-secret-key-please-change', algorithm='HS256') + precomptoken = ( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoibXkgZGF0YSIsInVzZXIiOiJUaW1vdGh5In0." + "8QqzQMJUTq0Dq7vHlnDjdoCKFPDAlvxGCpc_8XF41nI" + ) @hug.authentication.token def token_authentication(token): if token == precomptoken: - return 'Timothy' + return "Timothy" @hug.get(requires=token_authentication) def hello_world(): - return 'Hello World!' + return "Hello World!" - assert hug.test.get(api, 'hello_world', headers={'Authorization': precomptoken}).data == 'Hello World!' - assert '401' in hug.test.get(api, 'hello_world').status - assert '401' in hug.test.get(api, 'hello_world', headers={'Authorization': 'eyJhbGci'}).status + assert ( + hug.test.get(api, "hello_world", headers={"Authorization": precomptoken}).data + == "Hello World!" + ) + assert "401" in hug.test.get(api, "hello_world").status + assert "401" in hug.test.get(api, "hello_world", headers={"Authorization": "eyJhbGci"}).status - custom_context = dict(custom='context') + custom_context = dict(custom="context") @hug.context_factory() def create_test_context(*args, **kwargs): @@ -176,37 +214,42 @@ def create_test_context(*args, **kwargs): def delete_custom_context(context, exception=None, errors=None, lacks_requirement=None): assert context == custom_context assert not errors - context['exception'] = exception + context["exception"] = exception @hug.authentication.token def context_token_authentication(token, context): assert context == custom_context if token == precomptoken: - return 'Timothy' + return "Timothy" @hug.get(requires=context_token_authentication) def hello_context_world(): - return 'Hello context!' - - assert hug.test.get(api, 'hello_context_world', headers={'Authorization': precomptoken}).data == 'Hello context!' - assert not custom_context['exception'] - del custom_context['exception'] - assert '401' in hug.test.get(api, 'hello_context_world').status - assert isinstance(custom_context['exception'], HTTPUnauthorized) - del custom_context['exception'] - assert '401' in hug.test.get(api, 'hello_context_world', headers={'Authorization': 'eyJhbGci'}).status - assert isinstance(custom_context['exception'], HTTPUnauthorized) - del custom_context['exception'] + return "Hello context!" + + assert ( + hug.test.get(api, "hello_context_world", headers={"Authorization": precomptoken}).data + == "Hello context!" + ) + assert not custom_context["exception"] + del custom_context["exception"] + assert "401" in hug.test.get(api, "hello_context_world").status + assert isinstance(custom_context["exception"], HTTPUnauthorized) + del custom_context["exception"] + assert ( + "401" + in hug.test.get(api, "hello_context_world", headers={"Authorization": "eyJhbGci"}).status + ) + assert isinstance(custom_context["exception"], HTTPUnauthorized) + del custom_context["exception"] def test_documentation_carry_over(): """Test to ensure documentation correctly carries over - to address issue #252""" - authentication = hug.authentication.basic(hug.authentication.verify('User1', 'mypassword')) - assert authentication.__doc__ == 'Basic HTTP Authentication' + authentication = hug.authentication.basic(hug.authentication.verify("User1", "mypassword")) + assert authentication.__doc__ == "Basic HTTP Authentication" def test_missing_authenticator_docstring(): - @hug.authentication.authenticator def custom_authenticator(*args, **kwargs): return None @@ -215,6 +258,6 @@ def custom_authenticator(*args, **kwargs): @hug.get(requires=authentication) def hello_world(): - return 'Hello World!' + return "Hello World!" - hug.test.get(api, 'hello_world') + hug.test.get(api, "hello_world") diff --git a/tests/test_context_factory.py b/tests/test_context_factory.py index ef07b890..f3bc20bd 100644 --- a/tests/test_context_factory.py +++ b/tests/test_context_factory.py @@ -10,7 +10,6 @@ class RequirementFailed(object): - def __str__(self): return "requirement failed" @@ -20,9 +19,8 @@ class CustomException(Exception): class TestContextFactoryLocal(object): - def test_lack_requirement(self): - self.custom_context = dict(test='context') + self.custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -35,25 +33,25 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert not errors assert lacks_requirement assert isinstance(lacks_requirement, RequirementFailed) - self.custom_context['launched_delete_context'] = True + self.custom_context["launched_delete_context"] = True def test_local_requirement(**kwargs): - assert 'context' in kwargs - assert kwargs['context'] == self.custom_context - self.custom_context['launched_requirement'] = True + assert "context" in kwargs + assert kwargs["context"] == self.custom_context + self.custom_context["launched_requirement"] = True return RequirementFailed() @hug.local(requires=test_local_requirement) def requirement_local_function(): - self.custom_context['launched_local_function'] = True + self.custom_context["launched_local_function"] = True requirement_local_function() - assert 'launched_local_function' not in self.custom_context - assert 'launched_requirement' in self.custom_context - assert 'launched_delete_context' in self.custom_context + assert "launched_local_function" not in self.custom_context + assert "launched_requirement" in self.custom_context + assert "launched_delete_context" in self.custom_context def test_directive(self): - custom_context = dict(test='context') + custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -65,18 +63,18 @@ def delete_context(context, **kwargs): @hug.directive() def custom_directive(**kwargs): - assert 'context' in kwargs - assert kwargs['context'] == custom_context - return 'custom' + assert "context" in kwargs + assert kwargs["context"] == custom_context + return "custom" @hug.local() def directive_local_function(custom: custom_directive): - assert custom == 'custom' + assert custom == "custom" directive_local_function() def test_validation(self): - custom_context = dict(test='context', not_valid_number=43) + custom_context = dict(test="context", not_valid_number=43) @hug.context_factory() def return_context(**kwargs): @@ -88,31 +86,31 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert not exception assert errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True def test_requirement(**kwargs): - assert 'context' in kwargs - assert kwargs['context'] == custom_context - custom_context['launched_requirement'] = True + assert "context" in kwargs + assert kwargs["context"] == custom_context + custom_context["launched_requirement"] = True return RequirementFailed() @hug.type(extend=hug.types.number, accept_context=True) def custom_number_test(value, context): assert context == custom_context - if value == context['not_valid_number']: - raise ValueError('not valid number') + if value == context["not_valid_number"]: + raise ValueError("not valid number") return value @hug.local() def validation_local_function(value: custom_number_test): - custom_context['launched_local_function'] = value + custom_context["launched_local_function"] = value validation_local_function(43) - assert not 'launched_local_function' in custom_context - assert 'launched_delete_context' in custom_context + assert not "launched_local_function" in custom_context + assert "launched_delete_context" in custom_context def test_transform(self): - custom_context = dict(test='context', test_number=43) + custom_context = dict(test="context", test_number=43) @hug.context_factory() def return_context(**kwargs): @@ -124,26 +122,26 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert not exception assert not errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True class UserSchema(Schema): name = fields.Str() @post_dump() def check_context(self, data): - assert self.context['test'] == 'context' - self.context['test_number'] += 1 + assert self.context["test"] == "context" + self.context["test_number"] += 1 @hug.local() def validation_local_function() -> UserSchema(): - return {'name': 'test'} + return {"name": "test"} validation_local_function() - assert 'test_number' in custom_context and custom_context['test_number'] == 44 - assert 'launched_delete_context' in custom_context + assert "test_number" in custom_context and custom_context["test_number"] == 44 + assert "launched_delete_context" in custom_context def test_exception(self): - custom_context = dict(test='context') + custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -156,21 +154,21 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert isinstance(exception, CustomException) assert not errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True @hug.local() def exception_local_function(): - custom_context['launched_local_function'] = True + custom_context["launched_local_function"] = True raise CustomException() with pytest.raises(CustomException): exception_local_function() - assert 'launched_local_function' in custom_context - assert 'launched_delete_context' in custom_context + assert "launched_local_function" in custom_context + assert "launched_delete_context" in custom_context def test_success(self): - custom_context = dict(test='context') + custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -182,22 +180,21 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert not exception assert not errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True @hug.local() def success_local_function(): - custom_context['launched_local_function'] = True + custom_context["launched_local_function"] = True success_local_function() - assert 'launched_local_function' in custom_context - assert 'launched_delete_context' in custom_context + assert "launched_local_function" in custom_context + assert "launched_delete_context" in custom_context class TestContextFactoryCLI(object): - def test_lack_requirement(self): - custom_context = dict(test='context') + custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -210,25 +207,25 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert not errors assert lacks_requirement assert isinstance(lacks_requirement, RequirementFailed) - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True def test_requirement(**kwargs): - assert 'context' in kwargs - assert kwargs['context'] == custom_context - custom_context['launched_requirement'] = True + assert "context" in kwargs + assert kwargs["context"] == custom_context + custom_context["launched_requirement"] = True return RequirementFailed() @hug.cli(requires=test_requirement) def requirement_local_function(): - custom_context['launched_local_function'] = True + custom_context["launched_local_function"] = True hug.test.cli(requirement_local_function) - assert 'launched_local_function' not in custom_context - assert 'launched_requirement' in custom_context - assert 'launched_delete_context' in custom_context + assert "launched_local_function" not in custom_context + assert "launched_requirement" in custom_context + assert "launched_delete_context" in custom_context def test_directive(self): - custom_context = dict(test='context') + custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -240,18 +237,18 @@ def delete_context(context, **kwargs): @hug.directive() def custom_directive(**kwargs): - assert 'context' in kwargs - assert kwargs['context'] == custom_context - return 'custom' + assert "context" in kwargs + assert kwargs["context"] == custom_context + return "custom" @hug.cli() def directive_local_function(custom: custom_directive): - assert custom == 'custom' + assert custom == "custom" hug.test.cli(directive_local_function) def test_validation(self): - custom_context = dict(test='context', not_valid_number=43) + custom_context = dict(test="context", not_valid_number=43) @hug.context_factory() def return_context(**kwargs): @@ -263,33 +260,33 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert context == custom_context assert errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True def test_requirement(**kwargs): - assert 'context' in kwargs - assert kwargs['context'] == custom_context - custom_context['launched_requirement'] = True + assert "context" in kwargs + assert kwargs["context"] == custom_context + custom_context["launched_requirement"] = True return RequirementFailed() @hug.type(extend=hug.types.number, accept_context=True) def new_custom_number_test(value, context): assert context == custom_context - if value == context['not_valid_number']: - raise ValueError('not valid number') + if value == context["not_valid_number"]: + raise ValueError("not valid number") return value @hug.cli() def validation_local_function(value: hug.types.number): - custom_context['launched_local_function'] = value + custom_context["launched_local_function"] = value return 0 with pytest.raises(SystemExit): - hug.test.cli(validation_local_function, 'xxx') - assert 'launched_local_function' not in custom_context - assert 'launched_delete_context' in custom_context + hug.test.cli(validation_local_function, "xxx") + assert "launched_local_function" not in custom_context + assert "launched_delete_context" in custom_context def test_transform(self): - custom_context = dict(test='context', test_number=43) + custom_context = dict(test="context", test_number=43) @hug.context_factory() def return_context(**kwargs): @@ -301,29 +298,29 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert context == custom_context assert not errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True class UserSchema(Schema): name = fields.Str() @post_dump() def check_context(self, data): - assert self.context['test'] == 'context' - self.context['test_number'] += 1 + assert self.context["test"] == "context" + self.context["test_number"] += 1 @hug.cli() def transform_cli_function() -> UserSchema(): - custom_context['launched_cli_function'] = True - return {'name': 'test'} + custom_context["launched_cli_function"] = True + return {"name": "test"} hug.test.cli(transform_cli_function) - assert 'launched_cli_function' in custom_context - assert 'launched_delete_context' in custom_context - assert 'test_number' in custom_context - assert custom_context['test_number'] == 44 + assert "launched_cli_function" in custom_context + assert "launched_delete_context" in custom_context + assert "test_number" in custom_context + assert custom_context["test_number"] == 44 def test_exception(self): - custom_context = dict(test='context') + custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -336,20 +333,20 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert isinstance(exception, CustomException) assert not errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True @hug.cli() def exception_local_function(): - custom_context['launched_local_function'] = True + custom_context["launched_local_function"] = True raise CustomException() hug.test.cli(exception_local_function) - assert 'launched_local_function' in custom_context - assert 'launched_delete_context' in custom_context + assert "launched_local_function" in custom_context + assert "launched_delete_context" in custom_context def test_success(self): - custom_context = dict(test='context') + custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -361,22 +358,21 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert not exception assert not errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True @hug.cli() def success_local_function(): - custom_context['launched_local_function'] = True + custom_context["launched_local_function"] = True hug.test.cli(success_local_function) - assert 'launched_local_function' in custom_context - assert 'launched_delete_context' in custom_context + assert "launched_local_function" in custom_context + assert "launched_delete_context" in custom_context class TestContextFactoryHTTP(object): - def test_lack_requirement(self): - custom_context = dict(test='context') + custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -388,25 +384,25 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert not exception assert not errors assert lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True def test_requirement(**kwargs): - assert 'context' in kwargs - assert kwargs['context'] == custom_context - custom_context['launched_requirement'] = True - return 'requirement_failed' + assert "context" in kwargs + assert kwargs["context"] == custom_context + custom_context["launched_requirement"] = True + return "requirement_failed" - @hug.get('/requirement_function', requires=test_requirement) + @hug.get("/requirement_function", requires=test_requirement) def requirement_http_function(): - custom_context['launched_local_function'] = True + custom_context["launched_local_function"] = True - hug.test.get(module, '/requirement_function') - assert 'launched_local_function' not in custom_context - assert 'launched_requirement' in custom_context - assert 'launched_delete_context' in custom_context + hug.test.get(module, "/requirement_function") + assert "launched_local_function" not in custom_context + assert "launched_requirement" in custom_context + assert "launched_delete_context" in custom_context def test_directive(self): - custom_context = dict(test='context') + custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -418,18 +414,18 @@ def delete_context(context, **kwargs): @hug.directive() def custom_directive(**kwargs): - assert 'context' in kwargs - assert kwargs['context'] == custom_context - return 'custom' + assert "context" in kwargs + assert kwargs["context"] == custom_context + return "custom" - @hug.get('/directive_function') + @hug.get("/directive_function") def directive_http_function(custom: custom_directive): - assert custom == 'custom' + assert custom == "custom" - hug.test.get(module, '/directive_function') + hug.test.get(module, "/directive_function") def test_validation(self): - custom_context = dict(test='context', not_valid_number=43) + custom_context = dict(test="context", not_valid_number=43) @hug.context_factory() def return_context(**kwargs): @@ -441,31 +437,31 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert not exception assert errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True def test_requirement(**kwargs): - assert 'context' in kwargs - assert kwargs['context'] == custom_context - custom_context['launched_requirement'] = True + assert "context" in kwargs + assert kwargs["context"] == custom_context + custom_context["launched_requirement"] = True return RequirementFailed() @hug.type(extend=hug.types.number, accept_context=True) def custom_number_test(value, context): assert context == custom_context - if value == context['not_valid_number']: - raise ValueError('not valid number') + if value == context["not_valid_number"]: + raise ValueError("not valid number") return value - @hug.get('/validation_function') + @hug.get("/validation_function") def validation_http_function(value: custom_number_test): - custom_context['launched_local_function'] = value + custom_context["launched_local_function"] = value - hug.test.get(module, '/validation_function', 43) - assert 'launched_local_function ' not in custom_context - assert 'launched_delete_context' in custom_context + hug.test.get(module, "/validation_function", 43) + assert "launched_local_function " not in custom_context + assert "launched_delete_context" in custom_context def test_transform(self): - custom_context = dict(test='context', test_number=43) + custom_context = dict(test="context", test_number=43) @hug.context_factory() def return_context(**kwargs): @@ -477,28 +473,28 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert not exception assert not errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True class UserSchema(Schema): name = fields.Str() @post_dump() def check_context(self, data): - assert self.context['test'] == 'context' - self.context['test_number'] += 1 + assert self.context["test"] == "context" + self.context["test_number"] += 1 - @hug.get('/validation_function') + @hug.get("/validation_function") def validation_http_function() -> UserSchema(): - custom_context['launched_local_function'] = True + custom_context["launched_local_function"] = True - hug.test.get(module, '/validation_function', 43) - assert 'launched_local_function' in custom_context - assert 'launched_delete_context' in custom_context - assert 'test_number' in custom_context - assert custom_context['test_number'] == 44 + hug.test.get(module, "/validation_function", 43) + assert "launched_local_function" in custom_context + assert "launched_delete_context" in custom_context + assert "test_number" in custom_context + assert custom_context["test_number"] == 44 def test_exception(self): - custom_context = dict(test='context') + custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -511,21 +507,21 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert isinstance(exception, CustomException) assert not errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True - @hug.get('/exception_function') + @hug.get("/exception_function") def exception_http_function(): - custom_context['launched_local_function'] = True + custom_context["launched_local_function"] = True raise CustomException() with pytest.raises(CustomException): - hug.test.get(module, '/exception_function') + hug.test.get(module, "/exception_function") - assert 'launched_local_function' in custom_context - assert 'launched_delete_context' in custom_context + assert "launched_local_function" in custom_context + assert "launched_delete_context" in custom_context def test_success(self): - custom_context = dict(test='context') + custom_context = dict(test="context") @hug.context_factory() def return_context(**kwargs): @@ -537,13 +533,13 @@ def delete_context(context, exception=None, errors=None, lacks_requirement=None) assert not exception assert not errors assert not lacks_requirement - custom_context['launched_delete_context'] = True + custom_context["launched_delete_context"] = True - @hug.get('/success_function') + @hug.get("/success_function") def success_http_function(): - custom_context['launched_local_function'] = True + custom_context["launched_local_function"] = True - hug.test.get(module, '/success_function') + hug.test.get(module, "/success_function") - assert 'launched_local_function' in custom_context - assert 'launched_delete_context' in custom_context + assert "launched_local_function" in custom_context + assert "launched_delete_context" in custom_context diff --git a/tests/test_coroutines.py b/tests/test_coroutines.py index 3f03fa85..556f4fea 100644 --- a/tests/test_coroutines.py +++ b/tests/test_coroutines.py @@ -29,6 +29,7 @@ def test_basic_call_coroutine(): """The most basic Happy-Path test for Hug APIs using async""" + @hug.call() @asyncio.coroutine def hello_world(): @@ -39,10 +40,11 @@ def hello_world(): def test_nested_basic_call_coroutine(): """The most basic Happy-Path test for Hug APIs using async""" + @hug.call() @asyncio.coroutine def hello_world(): - return getattr(asyncio, 'ensure_future')(nested_hello_world()) + return getattr(asyncio, "ensure_future")(nested_hello_world()) @hug.local() @asyncio.coroutine @@ -54,8 +56,8 @@ def nested_hello_world(): def test_basic_call_on_method_coroutine(): """Test to ensure the most basic call still works if applied to a method""" - class API(object): + class API(object): @hug.call() @asyncio.coroutine def hello_world(self=None): @@ -64,13 +66,13 @@ def hello_world(self=None): api_instance = API() assert api_instance.hello_world.interface.http assert loop.run_until_complete(api_instance.hello_world()) == "Hello World!" - assert hug.test.get(api, '/hello_world').data == "Hello World!" + assert hug.test.get(api, "/hello_world").data == "Hello World!" def test_basic_call_on_method_through_api_instance_coroutine(): """Test to ensure the most basic call still works if applied to a method""" - class API(object): + class API(object): def hello_world(self): return "Hello World!" @@ -82,13 +84,13 @@ def hello_world(): return api_instance.hello_world() assert api_instance.hello_world() == "Hello World!" - assert hug.test.get(api, '/hello_world').data == "Hello World!" + assert hug.test.get(api, "/hello_world").data == "Hello World!" def test_basic_call_on_method_registering_without_decorator_coroutine(): """Test to ensure instance method calling via async works as expected""" - class API(object): + class API(object): def __init__(self): hug.call()(self.hello_world_method) @@ -99,4 +101,4 @@ def hello_world_method(self): api_instance = API() assert loop.run_until_complete(api_instance.hello_world_method()) == "Hello World!" - assert hug.test.get(api, '/hello_world_method').data == "Hello World!" + assert hug.test.get(api, "/hello_world_method").data == "Hello World!" diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 5c593fe6..e9e310f8 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -51,6 +51,7 @@ def test_basic_call(): """The most basic Happy-Path test for Hug APIs""" + @hug.call() def hello_world(): return "Hello World!" @@ -58,25 +59,24 @@ def hello_world(): assert hello_world() == "Hello World!" assert hello_world.interface.http - assert hug.test.get(api, '/hello_world').data == "Hello World!" - assert hug.test.get(module, '/hello_world').data == "Hello World!" + assert hug.test.get(api, "/hello_world").data == "Hello World!" + assert hug.test.get(module, "/hello_world").data == "Hello World!" def test_basic_call_on_method(hug_api): """Test to ensure the most basic call still works if applied to a method""" - class API(object): + class API(object): @hug.call(api=hug_api) def hello_world(self=None): return "Hello World!" api_instance = API() assert api_instance.hello_world.interface.http - assert api_instance.hello_world() == 'Hello World!' - assert hug.test.get(hug_api, '/hello_world').data == "Hello World!" + assert api_instance.hello_world() == "Hello World!" + assert hug.test.get(hug_api, "/hello_world").data == "Hello World!" class API(object): - def hello_world(self): return "Hello World!" @@ -86,11 +86,10 @@ def hello_world(self): def hello_world(): return api_instance.hello_world() - assert api_instance.hello_world() == 'Hello World!' - assert hug.test.get(hug_api, '/hello_world').data == "Hello World!" + assert api_instance.hello_world() == "Hello World!" + assert hug.test.get(hug_api, "/hello_world").data == "Hello World!" class API(object): - def __init__(self): hug.call(api=hug_api)(self.hello_world_method) @@ -99,53 +98,57 @@ def hello_world_method(self): api_instance = API() - assert api_instance.hello_world_method() == 'Hello World!' - assert hug.test.get(hug_api, '/hello_world_method').data == "Hello World!" + assert api_instance.hello_world_method() == "Hello World!" + assert hug.test.get(hug_api, "/hello_world_method").data == "Hello World!" def test_single_parameter(hug_api): """Test that an api with a single parameter interacts as desired""" + @hug.call(api=hug_api) def echo(text): return text - assert echo('Embrace') == 'Embrace' + assert echo("Embrace") == "Embrace" assert echo.interface.http with pytest.raises(TypeError): echo() - assert hug.test.get(hug_api, 'echo', text="Hello").data == "Hello" - assert 'required' in hug.test.get(hug_api, '/echo').data['errors']['text'].lower() + assert hug.test.get(hug_api, "echo", text="Hello").data == "Hello" + assert "required" in hug.test.get(hug_api, "/echo").data["errors"]["text"].lower() def test_on_invalid_transformer(): """Test to ensure it is possible to transform data when data is invalid""" - @hug.call(on_invalid=lambda data: 'error') + + @hug.call(on_invalid=lambda data: "error") def echo(text): return text - assert hug.test.get(api, '/echo').data == 'error' + assert hug.test.get(api, "/echo").data == "error" def handle_error(data, request, response): - return 'errored' + return "errored" @hug.call(on_invalid=handle_error) def echo2(text): return text - assert hug.test.get(api, '/echo2').data == 'errored' + + assert hug.test.get(api, "/echo2").data == "errored" def test_on_invalid_format(): """Test to ensure it's possible to change the format based on a validation error""" + @hug.get(output_invalid=hug.output_format.json, output=hug.output_format.file) def echo(text): return text - assert isinstance(hug.test.get(api, '/echo').data, dict) + assert isinstance(hug.test.get(api, "/echo").data, dict) def smart_output_type(response, request): if response and request: - return 'application/json' + return "application/json" @hug.format.content_type(smart_output_type) def output_formatter(data, request, response): @@ -155,11 +158,12 @@ def output_formatter(data, request, response): def echo2(text): return text - assert isinstance(hug.test.get(api, '/echo2').data, (list, tuple)) + assert isinstance(hug.test.get(api, "/echo2").data, (list, tuple)) def test_smart_redirect_routing(): """Test to ensure you can easily redirect to another method without an actual redirect""" + @hug.get() def implementation_1(): return 1 @@ -177,239 +181,280 @@ def smart_route(implementation: int): else: return "NOT IMPLEMENTED" - assert hug.test.get(api, 'smart_route', implementation=1).data == 1 - assert hug.test.get(api, 'smart_route', implementation=2).data == 2 - assert hug.test.get(api, 'smart_route', implementation=3).data == "NOT IMPLEMENTED" + assert hug.test.get(api, "smart_route", implementation=1).data == 1 + assert hug.test.get(api, "smart_route", implementation=2).data == 2 + assert hug.test.get(api, "smart_route", implementation=3).data == "NOT IMPLEMENTED" def test_custom_url(): """Test to ensure that it's possible to have a route that differs from the function name""" - @hug.call('/custom_route') + + @hug.call("/custom_route") def method_name(): - return 'works' + return "works" - assert hug.test.get(api, 'custom_route').data == 'works' + assert hug.test.get(api, "custom_route").data == "works" def test_api_auto_initiate(): """Test to ensure that Hug automatically exposes a wsgi server method""" - assert isinstance(__hug_wsgi__(create_environ('/non_existant'), StartResponseMock()), (list, tuple)) + assert isinstance( + __hug_wsgi__(create_environ("/non_existant"), StartResponseMock()), (list, tuple) + ) def test_parameters(): """Tests to ensure that Hug can easily handle multiple parameters with multiple types""" - @hug.call() - def multiple_parameter_types(start, middle: hug.types.text, end: hug.types.number=5, **kwargs): - return 'success' - - assert hug.test.get(api, 'multiple_parameter_types', start='start', middle='middle', end=7).data == 'success' - assert hug.test.get(api, 'multiple_parameter_types', start='start', middle='middle').data == 'success' - assert hug.test.get(api, 'multiple_parameter_types', start='start', middle='middle', other="yo").data == 'success' - nan_test = hug.test.get(api, 'multiple_parameter_types', start='start', middle='middle', end='NAN').data - assert 'Invalid' in nan_test['errors']['end'] + @hug.call() + def multiple_parameter_types( + start, middle: hug.types.text, end: hug.types.number = 5, **kwargs + ): + return "success" + + assert ( + hug.test.get(api, "multiple_parameter_types", start="start", middle="middle", end=7).data + == "success" + ) + assert ( + hug.test.get(api, "multiple_parameter_types", start="start", middle="middle").data + == "success" + ) + assert ( + hug.test.get( + api, "multiple_parameter_types", start="start", middle="middle", other="yo" + ).data + == "success" + ) + + nan_test = hug.test.get( + api, "multiple_parameter_types", start="start", middle="middle", end="NAN" + ).data + assert "Invalid" in nan_test["errors"]["end"] def test_raise_on_invalid(): """Test to ensure hug correctly respects a request to allow validations errors to pass through as exceptions""" + @hug.get(raise_on_invalid=True) def my_handler(argument_1: int): return True with pytest.raises(Exception): - hug.test.get(api, 'my_handler', argument_1='hi') + hug.test.get(api, "my_handler", argument_1="hi") - assert hug.test.get(api, 'my_handler', argument_1=1) + assert hug.test.get(api, "my_handler", argument_1=1) def test_range_request(): """Test to ensure that requesting a range works as expected""" + @hug.get(output=hug.output_format.png_image) def image(): - return 'artwork/logo.png' + return "artwork/logo.png" + + assert hug.test.get(api, "image", headers={"range": "bytes=0-100"}) + assert hug.test.get(api, "image", headers={"range": "bytes=0--1"}) - assert hug.test.get(api, 'image', headers={'range': 'bytes=0-100'}) - assert hug.test.get(api, 'image', headers={'range': 'bytes=0--1'}) def test_parameters_override(): """Test to ensure the parameters override is handled as expected""" - @hug.get(parameters=('parameter1', 'parameter2')) + + @hug.get(parameters=("parameter1", "parameter2")) def test_call(**kwargs): return kwargs - assert hug.test.get(api, 'test_call', parameter1='one', parameter2='two').data == {'parameter1': 'one', - 'parameter2': 'two'} + assert hug.test.get(api, "test_call", parameter1="one", parameter2="two").data == { + "parameter1": "one", + "parameter2": "two", + } def test_parameter_injection(): """Tests that hug correctly auto injects variables such as request and response""" + @hug.call() def inject_request(request): - return request and 'success' - assert hug.test.get(api, 'inject_request').data == 'success' + return request and "success" + + assert hug.test.get(api, "inject_request").data == "success" @hug.call() def inject_response(response): - return response and 'success' - assert hug.test.get(api, 'inject_response').data == 'success' + return response and "success" + + assert hug.test.get(api, "inject_response").data == "success" @hug.call() def inject_both(request, response): - return request and response and 'success' - assert hug.test.get(api, 'inject_both').data == 'success' + return request and response and "success" + + assert hug.test.get(api, "inject_both").data == "success" @hug.call() def wont_appear_in_kwargs(**kwargs): - return 'request' not in kwargs and 'response' not in kwargs and 'success' - assert hug.test.get(api, 'wont_appear_in_kwargs').data == 'success' + return "request" not in kwargs and "response" not in kwargs and "success" + + assert hug.test.get(api, "wont_appear_in_kwargs").data == "success" def test_method_routing(): """Test that all hugs HTTP routers correctly route methods to the correct handler""" + @hug.get() def method_get(): - return 'GET' + return "GET" @hug.post() def method_post(): - return 'POST' + return "POST" @hug.connect() def method_connect(): - return 'CONNECT' + return "CONNECT" @hug.delete() def method_delete(): - return 'DELETE' + return "DELETE" @hug.options() def method_options(): - return 'OPTIONS' + return "OPTIONS" @hug.put() def method_put(): - return 'PUT' + return "PUT" @hug.trace() def method_trace(): - return 'TRACE' + return "TRACE" - assert hug.test.get(api, 'method_get').data == 'GET' - assert hug.test.post(api, 'method_post').data == 'POST' - assert hug.test.connect(api, 'method_connect').data == 'CONNECT' - assert hug.test.delete(api, 'method_delete').data == 'DELETE' - assert hug.test.options(api, 'method_options').data == 'OPTIONS' - assert hug.test.put(api, 'method_put').data == 'PUT' - assert hug.test.trace(api, 'method_trace').data == 'TRACE' + assert hug.test.get(api, "method_get").data == "GET" + assert hug.test.post(api, "method_post").data == "POST" + assert hug.test.connect(api, "method_connect").data == "CONNECT" + assert hug.test.delete(api, "method_delete").data == "DELETE" + assert hug.test.options(api, "method_options").data == "OPTIONS" + assert hug.test.put(api, "method_put").data == "PUT" + assert hug.test.trace(api, "method_trace").data == "TRACE" - @hug.call(accept=('GET', 'POST')) + @hug.call(accept=("GET", "POST")) def accepts_get_and_post(): - return 'success' + return "success" - assert hug.test.get(api, 'accepts_get_and_post').data == 'success' - assert hug.test.post(api, 'accepts_get_and_post').data == 'success' - assert 'method not allowed' in hug.test.trace(api, 'accepts_get_and_post').status.lower() + assert hug.test.get(api, "accepts_get_and_post").data == "success" + assert hug.test.post(api, "accepts_get_and_post").data == "success" + assert "method not allowed" in hug.test.trace(api, "accepts_get_and_post").status.lower() def test_not_found(hug_api): """Test to ensure the not_found decorator correctly routes 404s to the correct handler""" + @hug.not_found(api=hug_api) def not_found_handler(): return "Not Found" - result = hug.test.get(hug_api, '/does_not_exist/yet') + result = hug.test.get(hug_api, "/does_not_exist/yet") assert result.data == "Not Found" assert result.status == falcon.HTTP_NOT_FOUND @hug.not_found(versions=10, api=hug_api) # noqa def not_found_handler(response): response.status = falcon.HTTP_OK - return {'look': 'elsewhere'} + return {"look": "elsewhere"} - result = hug.test.get(hug_api, '/v10/does_not_exist/yet') - assert result.data == {'look': 'elsewhere'} + result = hug.test.get(hug_api, "/v10/does_not_exist/yet") + assert result.data == {"look": "elsewhere"} assert result.status == falcon.HTTP_OK - result = hug.test.get(hug_api, '/does_not_exist/yet') + result = hug.test.get(hug_api, "/does_not_exist/yet") assert result.data == "Not Found" assert result.status == falcon.HTTP_NOT_FOUND hug_api.http.output_format = hug.output_format.text - result = hug.test.get(hug_api, '/v10/does_not_exist/yet') + result = hug.test.get(hug_api, "/v10/does_not_exist/yet") assert result.data == "{'look': 'elsewhere'}" def test_not_found_with_extended_api(): """Test to ensure the not_found decorator works correctly when the API is extended""" + @hug.extend_api() def extend_with(): import tests.module_fake - return (tests.module_fake, ) - assert hug.test.get(api, '/does_not_exist/yet').data is True + return (tests.module_fake,) + + assert hug.test.get(api, "/does_not_exist/yet").data is True + def test_versioning(): """Ensure that Hug correctly routes API functions based on version""" - @hug.get('/echo') + + @hug.get("/echo") def echo(text): return "Not Implemented" - @hug.get('/echo', versions=1) # noqa + @hug.get("/echo", versions=1) # noqa def echo(text): return text - @hug.get('/echo', versions=range(2, 4)) # noqa + @hug.get("/echo", versions=range(2, 4)) # noqa def echo(text): return "Echo: {text}".format(**locals()) - @hug.get('/echo', versions=7) # noqa + @hug.get("/echo", versions=7) # noqa def echo(text, api_version): return api_version - @hug.get('/echo', versions='8') # noqa + @hug.get("/echo", versions="8") # noqa def echo(text, api_version): return api_version - @hug.get('/echo', versions=False) # noqa + @hug.get("/echo", versions=False) # noqa def echo(text): return "No Versions" with pytest.raises(ValueError): - @hug.get('/echo', versions='eight') # noqa + + @hug.get("/echo", versions="eight") # noqa def echo(text, api_version): return api_version - assert hug.test.get(api, 'v1/echo', text="hi").data == 'hi' - assert hug.test.get(api, 'v2/echo', text="hi").data == "Echo: hi" - assert hug.test.get(api, 'v3/echo', text="hi").data == "Echo: hi" - assert hug.test.get(api, 'echo', text="hi", api_version=3).data == "Echo: hi" - assert hug.test.get(api, 'echo', text="hi", headers={'X-API-VERSION': '3'}).data == "Echo: hi" - assert hug.test.get(api, 'v4/echo', text="hi").data == "Not Implemented" - assert hug.test.get(api, 'v7/echo', text="hi").data == 7 - assert hug.test.get(api, 'v8/echo', text="hi").data == 8 - assert hug.test.get(api, 'echo', text="hi").data == "No Versions" - assert hug.test.get(api, 'echo', text="hi", api_version=3, body={'api_vertion': 4}).data == "Echo: hi" + assert hug.test.get(api, "v1/echo", text="hi").data == "hi" + assert hug.test.get(api, "v2/echo", text="hi").data == "Echo: hi" + assert hug.test.get(api, "v3/echo", text="hi").data == "Echo: hi" + assert hug.test.get(api, "echo", text="hi", api_version=3).data == "Echo: hi" + assert hug.test.get(api, "echo", text="hi", headers={"X-API-VERSION": "3"}).data == "Echo: hi" + assert hug.test.get(api, "v4/echo", text="hi").data == "Not Implemented" + assert hug.test.get(api, "v7/echo", text="hi").data == 7 + assert hug.test.get(api, "v8/echo", text="hi").data == 8 + assert hug.test.get(api, "echo", text="hi").data == "No Versions" + assert ( + hug.test.get(api, "echo", text="hi", api_version=3, body={"api_vertion": 4}).data + == "Echo: hi" + ) with pytest.raises(ValueError): - hug.test.get(api, 'v4/echo', text="hi", api_version=3) + hug.test.get(api, "v4/echo", text="hi", api_version=3) def test_multiple_version_injection(): """Test to ensure that the version injected sticks when calling other functions within an API""" + @hug.get(versions=(1, 2, None)) def my_api_function(hug_api_version): return hug_api_version - assert hug.test.get(api, 'v1/my_api_function').data == 1 - assert hug.test.get(api, 'v2/my_api_function').data == 2 - assert hug.test.get(api, 'v3/my_api_function').data == 3 + assert hug.test.get(api, "v1/my_api_function").data == 1 + assert hug.test.get(api, "v2/my_api_function").data == 2 + assert hug.test.get(api, "v3/my_api_function").data == 3 @hug.get(versions=(None, 1)) @hug.local(version=1) def call_other_function(hug_current_api): return hug_current_api.my_api_function() - assert hug.test.get(api, 'v1/call_other_function').data == 1 + assert hug.test.get(api, "v1/call_other_function").data == 1 assert call_other_function() == 1 @hug.get(versions=1) @@ -417,59 +462,68 @@ def call_other_function(hug_current_api): def one_more_level_of_indirection(hug_current_api): return hug_current_api.call_other_function() - assert hug.test.get(api, 'v1/one_more_level_of_indirection').data == 1 + assert hug.test.get(api, "v1/one_more_level_of_indirection").data == 1 assert one_more_level_of_indirection() == 1 def test_json_auto_convert(): """Test to ensure all types of data correctly auto convert into json""" - @hug.get('/test_json') + + @hug.get("/test_json") def test_json(text): return text - assert hug.test.get(api, 'test_json', body={'text': 'value'}).data == "value" - @hug.get('/test_json_body') + assert hug.test.get(api, "test_json", body={"text": "value"}).data == "value" + + @hug.get("/test_json_body") def test_json_body(body): return body - assert hug.test.get(api, 'test_json_body', body=['value1', 'value2']).data == ['value1', 'value2'] + + assert hug.test.get(api, "test_json_body", body=["value1", "value2"]).data == [ + "value1", + "value2", + ] @hug.get(parse_body=False) def test_json_body_stream_only(body=None): return body - assert hug.test.get(api, 'test_json_body_stream_only', body=['value1', 'value2']).data is None + + assert hug.test.get(api, "test_json_body_stream_only", body=["value1", "value2"]).data is None def test_error_handling(): """Test to ensure Hug correctly handles Falcon errors that are thrown during processing""" + @hug.get() def test_error(): - raise falcon.HTTPInternalServerError('Failed', 'For Science!') + raise falcon.HTTPInternalServerError("Failed", "For Science!") - response = hug.test.get(api, 'test_error') - assert 'errors' in response.data - assert response.data['errors']['Failed'] == 'For Science!' + response = hug.test.get(api, "test_error") + assert "errors" in response.data + assert response.data["errors"]["Failed"] == "For Science!" def test_error_handling_builtin_exception(): """Test to ensure built in exception types errors are handled as expected""" + def raise_error(value): - raise KeyError('Invalid value') + raise KeyError("Invalid value") @hug.get() def test_error(data: raise_error): return True - response = hug.test.get(api, 'test_error', data=1) - assert 'errors' in response.data - assert response.data['errors']['data'] == 'Invalid value' + response = hug.test.get(api, "test_error", data=1) + assert "errors" in response.data + assert response.data["errors"]["data"] == "Invalid value" def test_error_handling_custom(): """Test to ensure custom exceptions work as expected""" - class Error(Exception): + class Error(Exception): def __str__(self): - return 'Error' + return "Error" def raise_error(value): raise Error() @@ -478,38 +532,41 @@ def raise_error(value): def test_error(data: raise_error): return True - response = hug.test.get(api, 'test_error', data=1) - assert 'errors' in response.data - assert response.data['errors']['data'] == 'Error' + response = hug.test.get(api, "test_error", data=1) + assert "errors" in response.data + assert response.data["errors"]["data"] == "Error" def test_return_modifer(): """Ensures you can modify the output of a HUG API using -> annotation""" + @hug.get() def hello() -> lambda data: "Hello {0}!".format(data): return "world" - assert hug.test.get(api, 'hello').data == "Hello world!" - assert hello() == 'world' + assert hug.test.get(api, "hello").data == "Hello world!" + assert hello() == "world" @hug.get(transform=lambda data: "Goodbye {0}!".format(data)) def hello() -> lambda data: "Hello {0}!".format(data): return "world" - assert hug.test.get(api, 'hello').data == "Goodbye world!" - assert hello() == 'world' + + assert hug.test.get(api, "hello").data == "Goodbye world!" + assert hello() == "world" @hug.get() def hello() -> str: return "world" - assert hug.test.get(api, 'hello').data == "world" - assert hello() == 'world' + + assert hug.test.get(api, "hello").data == "world" + assert hello() == "world" @hug.get(transform=False) def hello() -> lambda data: "Hello {0}!".format(data): return "world" - assert hug.test.get(api, 'hello').data == "world" - assert hello() == 'world' + assert hug.test.get(api, "hello").data == "world" + assert hello() == "world" def transform_with_request_data(data, request, response): return (data, request and True, response and True) @@ -518,36 +575,37 @@ def transform_with_request_data(data, request, response): def hello(): return "world" - response = hug.test.get(api, 'hello') - assert response.data == ['world', True, True] + response = hug.test.get(api, "hello") + assert response.data == ["world", True, True] def test_custom_deserializer_support(): """Ensure that custom desirializers work as expected""" + class CustomDeserializer(object): def from_string(self, string): - return 'custom {}'.format(string) + return "custom {}".format(string) @hug.get() def test_custom_deserializer(text: CustomDeserializer()): return text - assert hug.test.get(api, 'test_custom_deserializer', text='world').data == 'custom world' + assert hug.test.get(api, "test_custom_deserializer", text="world").data == "custom world" -@pytest.mark.skipif(MARSHMALLOW_MAJOR_VERSION != 2, reason='This test is for marshmallow 2 only') +@pytest.mark.skipif(MARSHMALLOW_MAJOR_VERSION != 2, reason="This test is for marshmallow 2 only") def test_marshmallow2_support(): """Ensure that you can use Marshmallow style objects to control input and output validation and transformation""" - MarshalResult = namedtuple('MarshalResult', ['data', 'errors']) + MarshalResult = namedtuple("MarshalResult", ["data", "errors"]) class MarshmallowStyleObject(object): def dump(self, item): - if item == 'bad': - return MarshalResult('', 'problems') - return MarshalResult('Dump Success', {}) + if item == "bad": + return MarshalResult("", "problems") + return MarshalResult("Dump Success", {}) def load(self, item): - return ('Load Success', None) + return ("Load Success", None) def loads(self, item): return self.load(item) @@ -556,33 +614,31 @@ def loads(self, item): @hug.get() def test_marshmallow_style() -> schema: - return 'world' - - assert hug.test.get(api, 'test_marshmallow_style').data == "Dump Success" - assert test_marshmallow_style() == 'world' + return "world" + assert hug.test.get(api, "test_marshmallow_style").data == "Dump Success" + assert test_marshmallow_style() == "world" @hug.get() def test_marshmallow_style_error() -> schema: - return 'bad' + return "bad" with pytest.raises(InvalidTypeData): - hug.test.get(api, 'test_marshmallow_style_error') - + hug.test.get(api, "test_marshmallow_style_error") @hug.get() def test_marshmallow_input(item: schema): return item - assert hug.test.get(api, 'test_marshmallow_input', item='bacon').data == "Load Success" - assert test_marshmallow_style() == 'world' + assert hug.test.get(api, "test_marshmallow_input", item="bacon").data == "Load Success" + assert test_marshmallow_style() == "world" class MarshmallowStyleObjectWithError(object): def dump(self, item): - return 'Dump Success' + return "Dump Success" def load(self, item): - return ('Load Success', {'type': 'invalid'}) + return ("Load Success", {"type": "invalid"}) def loads(self, item): return self.load(item) @@ -593,7 +649,9 @@ def loads(self, item): def test_marshmallow_input2(item: schema): return item - assert hug.test.get(api, 'test_marshmallow_input2', item='bacon').data == {'errors': {'item': {'type': 'invalid'}}} + assert hug.test.get(api, "test_marshmallow_input2", item="bacon").data == { + "errors": {"item": {"type": "invalid"}} + } class MarshmallowStyleField(object): def deserialize(self, value): @@ -603,21 +661,21 @@ def deserialize(self, value): def test_marshmallow_input_field(item: MarshmallowStyleField()): return item - assert hug.test.get(api, 'test_marshmallow_input_field', item=1).data == '1' + assert hug.test.get(api, "test_marshmallow_input_field", item=1).data == "1" -@pytest.mark.skipif(MARSHMALLOW_MAJOR_VERSION != 3, reason='This test is for marshmallow 3 only') +@pytest.mark.skipif(MARSHMALLOW_MAJOR_VERSION != 3, reason="This test is for marshmallow 3 only") def test_marshmallow3_support(): """Ensure that you can use Marshmallow style objects to control input and output validation and transformation""" class MarshmallowStyleObject(object): def dump(self, item): - if item == 'bad': - raise ValidationError('problems') - return 'Dump Success' + if item == "bad": + raise ValidationError("problems") + return "Dump Success" def load(self, item): - return 'Load Success' + return "Load Success" def loads(self, item): return self.load(item) @@ -626,33 +684,31 @@ def loads(self, item): @hug.get() def test_marshmallow_style() -> schema: - return 'world' - - assert hug.test.get(api, 'test_marshmallow_style').data == "Dump Success" - assert test_marshmallow_style() == 'world' + return "world" + assert hug.test.get(api, "test_marshmallow_style").data == "Dump Success" + assert test_marshmallow_style() == "world" @hug.get() def test_marshmallow_style_error() -> schema: - return 'bad' + return "bad" with pytest.raises(InvalidTypeData): - hug.test.get(api, 'test_marshmallow_style_error') - + hug.test.get(api, "test_marshmallow_style_error") @hug.get() def test_marshmallow_input(item: schema): return item - assert hug.test.get(api, 'test_marshmallow_input', item='bacon').data == "Load Success" - assert test_marshmallow_style() == 'world' + assert hug.test.get(api, "test_marshmallow_input", item="bacon").data == "Load Success" + assert test_marshmallow_style() == "world" class MarshmallowStyleObjectWithError(object): def dump(self, item): - return 'Dump Success' + return "Dump Success" def load(self, item): - raise ValidationError({'type': 'invalid'}) + raise ValidationError({"type": "invalid"}) def loads(self, item): return self.load(item) @@ -663,7 +719,9 @@ def loads(self, item): def test_marshmallow_input2(item: schema): return item - assert hug.test.get(api, 'test_marshmallow_input2', item='bacon').data == {'errors': {'item': {'type': 'invalid'}}} + assert hug.test.get(api, "test_marshmallow_input2", item="bacon").data == { + "errors": {"item": {"type": "invalid"}} + } class MarshmallowStyleField(object): def deserialize(self, value): @@ -673,23 +731,25 @@ def deserialize(self, value): def test_marshmallow_input_field(item: MarshmallowStyleField()): return item - assert hug.test.get(api, 'test_marshmallow_input_field', item=1).data == '1' + assert hug.test.get(api, "test_marshmallow_input_field", item=1).data == "1" def test_stream_return(): """Test to ensure that its valid for a hug API endpoint to return a stream""" + @hug.get(output=hug.output_format.text) def test(): - return open(os.path.join(BASE_DIRECTORY, 'README.md'), 'rb') + return open(os.path.join(BASE_DIRECTORY, "README.md"), "rb") - assert 'hug' in hug.test.get(api, 'test').data + assert "hug" in hug.test.get(api, "test").data def test_smart_outputter(): """Test to ensure that the output formatter can accept request and response arguments""" + def smart_output_type(response, request): if response and request: - return 'application/json' + return "application/json" @hug.format.content_type(smart_output_type) def output_formatter(data, request, response): @@ -699,48 +759,48 @@ def output_formatter(data, request, response): def test(): return True - assert hug.test.get(api, 'test').data == [True, True, True] + assert hug.test.get(api, "test").data == [True, True, True] -@pytest.mark.skipif(sys.platform == 'win32', reason='Currently failing on Windows build') +@pytest.mark.skipif(sys.platform == "win32", reason="Currently failing on Windows build") def test_output_format(hug_api): """Test to ensure it's possible to quickly change the default hug output format""" old_formatter = api.http.output_format @hug.default_output_format() def augmented(data): - return hug.output_format.json(['Augmented', data]) + return hug.output_format.json(["Augmented", data]) @hug.cli() - @hug.get(suffixes=('.js', '/js'), prefixes='/text') + @hug.get(suffixes=(".js", "/js"), prefixes="/text") def hello(): return "world" - assert hug.test.get(api, 'hello').data == ['Augmented', 'world'] - assert hug.test.get(api, 'hello.js').data == ['Augmented', 'world'] - assert hug.test.get(api, 'hello/js').data == ['Augmented', 'world'] - assert hug.test.get(api, 'text/hello').data == ['Augmented', 'world'] - assert hug.test.cli('hello', api=api) == 'world' + assert hug.test.get(api, "hello").data == ["Augmented", "world"] + assert hug.test.get(api, "hello.js").data == ["Augmented", "world"] + assert hug.test.get(api, "hello/js").data == ["Augmented", "world"] + assert hug.test.get(api, "text/hello").data == ["Augmented", "world"] + assert hug.test.cli("hello", api=api) == "world" @hug.default_output_format(cli=True, http=False, api=hug_api) def augmented(data): - return hug.output_format.json(['Augmented', data]) + return hug.output_format.json(["Augmented", data]) @hug.cli(api=hug_api) def hello(): return "world" - assert hug.test.cli('hello', api=hug_api) == ['Augmented', 'world'] + assert hug.test.cli("hello", api=hug_api) == ["Augmented", "world"] @hug.default_output_format(cli=True, http=False, api=hug_api, apply_globally=True) def augmented(data): - return hug.output_format.json(['Augmented2', data]) + return hug.output_format.json(["Augmented2", data]) @hug.cli(api=api) def hello(): return "world" - assert hug.test.cli('hello', api=api) == ['Augmented2', 'world'] + assert hug.test.cli("hello", api=api) == ["Augmented2", "world"] hug.defaults.cli_output_format = hug.output_format.text @hug.default_output_format() @@ -751,125 +811,138 @@ def jsonify(data): @hug.get() def my_method(): - return {'Should': 'work'} + return {"Should": "work"} - assert hug.test.get(api, 'my_method').data == "{'Should': 'work'}" + assert hug.test.get(api, "my_method").data == "{'Should': 'work'}" api.http.output_format = old_formatter -@pytest.mark.skipif(sys.platform == 'win32', reason='Currently failing on Windows build') +@pytest.mark.skipif(sys.platform == "win32", reason="Currently failing on Windows build") def test_input_format(): """Test to ensure it's possible to quickly change the default hug output format""" - old_format = api.http.input_format('application/json') - api.http.set_input_format('application/json', lambda a, **headers: {'no': 'relation'}) + old_format = api.http.input_format("application/json") + api.http.set_input_format("application/json", lambda a, **headers: {"no": "relation"}) @hug.get() def hello(body): return body - assert hug.test.get(api, 'hello', body={'should': 'work'}).data == {'no': 'relation'} + assert hug.test.get(api, "hello", body={"should": "work"}).data == {"no": "relation"} @hug.get() def hello2(body): return body - assert not hug.test.get(api, 'hello2').data + assert not hug.test.get(api, "hello2").data - api.http.set_input_format('application/json', old_format) + api.http.set_input_format("application/json", old_format) -@pytest.mark.skipif(sys.platform == 'win32', reason='Currently failing on Windows build') +@pytest.mark.skipif(sys.platform == "win32", reason="Currently failing on Windows build") def test_specific_input_format(): """Test to ensure the input formatter can be specified""" - @hug.get(inputs={'application/json': lambda a, **headers: 'formatted'}) + + @hug.get(inputs={"application/json": lambda a, **headers: "formatted"}) def hello(body): return body - assert hug.test.get(api, 'hello', body={'should': 'work'}).data == 'formatted' + assert hug.test.get(api, "hello", body={"should": "work"}).data == "formatted" -@pytest.mark.skipif(sys.platform == 'win32', reason='Currently failing on Windows build') +@pytest.mark.skipif(sys.platform == "win32", reason="Currently failing on Windows build") def test_content_type_with_parameter(): """Test a Content-Type with parameter as `application/json charset=UTF-8` as described in https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7""" + @hug.get() def demo(body): return body - assert hug.test.get(api, 'demo', body={}, headers={'content-type': 'application/json'}).data == {} - assert hug.test.get(api, 'demo', body={}, headers={'content-type': 'application/json; charset=UTF-8'}).data == {} + assert ( + hug.test.get(api, "demo", body={}, headers={"content-type": "application/json"}).data == {} + ) + assert ( + hug.test.get( + api, "demo", body={}, headers={"content-type": "application/json; charset=UTF-8"} + ).data + == {} + ) -@pytest.mark.skipif(sys.platform == 'win32', reason='Currently failing on Windows build') +@pytest.mark.skipif(sys.platform == "win32", reason="Currently failing on Windows build") def test_middleware(): """Test to ensure the basic concept of a middleware works as expected""" + @hug.request_middleware() def proccess_data(request, response): - request.env['SERVER_NAME'] = 'Bacon' + request.env["SERVER_NAME"] = "Bacon" @hug.response_middleware() def proccess_data2(request, response, resource): - response.set_header('Bacon', 'Yumm') + response.set_header("Bacon", "Yumm") @hug.reqresp_middleware() def process_data3(request): - request.env['MEET'] = 'Ham' + request.env["MEET"] = "Ham" response, resource = yield request - response.set_header('Ham', 'Buu!!') + response.set_header("Ham", "Buu!!") yield response @hug.get() def hello(request): - return [ - request.env['SERVER_NAME'], - request.env['MEET'] - ] + return [request.env["SERVER_NAME"], request.env["MEET"]] - result = hug.test.get(api, 'hello') - assert result.data == ['Bacon', 'Ham'] - assert result.headers_dict['Bacon'] == 'Yumm' - assert result.headers_dict['Ham'] == 'Buu!!' + result = hug.test.get(api, "hello") + assert result.data == ["Bacon", "Ham"] + assert result.headers_dict["Bacon"] == "Yumm" + assert result.headers_dict["Ham"] == "Buu!!" def test_requires(): """Test to ensure only if requirements successfully keep calls from happening""" + def user_is_not_tim(request, response, **kwargs): - if request.headers.get('USER', '') != 'Tim': + if request.headers.get("USER", "") != "Tim": return True - return 'Unauthorized' + return "Unauthorized" @hug.get(requires=user_is_not_tim) def hello(request): - return 'Hi!' + return "Hi!" - assert hug.test.get(api, 'hello').data == 'Hi!' - assert hug.test.get(api, 'hello', headers={'USER': 'Tim'}).data == 'Unauthorized' + assert hug.test.get(api, "hello").data == "Hi!" + assert hug.test.get(api, "hello", headers={"USER": "Tim"}).data == "Unauthorized" def test_extending_api(): """Test to ensure it's possible to extend the current API from an external file""" - @hug.extend_api('/fake') + + @hug.extend_api("/fake") def extend_with(): import tests.module_fake - return (tests.module_fake, ) - @hug.get('/fake/error') + return (tests.module_fake,) + + @hug.get("/fake/error") def my_error(): import tests.module_fake + raise tests.module_fake.FakeException() - assert hug.test.get(api, 'fake/made_up_api').data - assert hug.test.get(api, 'fake/error').data == True + assert hug.test.get(api, "fake/made_up_api").data + assert hug.test.get(api, "fake/error").data == True def test_extending_api_simple(): """Test to ensure it's possible to extend the current API from an external file with just one API endpoint""" - @hug.extend_api('/fake_simple') + + @hug.extend_api("/fake_simple") def extend_with(): import tests.module_fake_simple - return (tests.module_fake_simple, ) - assert hug.test.get(api, 'fake_simple/made_up_hello').data == 'hello' + return (tests.module_fake_simple,) + + assert hug.test.get(api, "fake_simple/made_up_hello").data == "hello" def test_extending_api_with_exception_handler(): @@ -879,233 +952,250 @@ def test_extending_api_with_exception_handler(): @hug.exception(FakeSimpleException) def handle_exception(exception): - return 'it works!' + return "it works!" - @hug.extend_api('/fake_simple') + @hug.extend_api("/fake_simple") def extend_with(): import tests.module_fake_simple - return (tests.module_fake_simple, ) - assert hug.test.get(api, '/fake_simple/exception').data == 'it works!' + return (tests.module_fake_simple,) + + assert hug.test.get(api, "/fake_simple/exception").data == "it works!" def test_extending_api_with_base_url(): """Test to ensure it's possible to extend the current API with a specified base URL""" - @hug.extend_api('/fake', base_url='/api') + + @hug.extend_api("/fake", base_url="/api") def extend_with(): import tests.module_fake - return (tests.module_fake, ) - assert hug.test.get(api, '/api/v1/fake/made_up_api').data + return (tests.module_fake,) + + assert hug.test.get(api, "/api/v1/fake/made_up_api").data def test_extending_api_with_same_path_under_different_base_url(): """Test to ensure it's possible to extend the current API with the same path under a different base URL""" + @hug.get() def made_up_hello(): - return 'hi' + return "hi" - @hug.extend_api(base_url='/api') + @hug.extend_api(base_url="/api") def extend_with(): import tests.module_fake_simple - return (tests.module_fake_simple, ) - assert hug.test.get(api, '/made_up_hello').data == 'hi' - assert hug.test.get(api, '/api/made_up_hello').data == 'hello' + return (tests.module_fake_simple,) + + assert hug.test.get(api, "/made_up_hello").data == "hi" + assert hug.test.get(api, "/api/made_up_hello").data == "hello" def test_extending_api_with_methods_in_one_module(): """Test to ensure it's possible to extend the current API with HTTP methods for a view in one module""" - @hug.extend_api(base_url='/get_and_post') + + @hug.extend_api(base_url="/get_and_post") def extend_with(): import tests.module_fake_many_methods + return (tests.module_fake_many_methods,) - assert hug.test.get(api, '/get_and_post/made_up_hello').data == 'hello from GET' - assert hug.test.post(api, '/get_and_post/made_up_hello').data == 'hello from POST' + assert hug.test.get(api, "/get_and_post/made_up_hello").data == "hello from GET" + assert hug.test.post(api, "/get_and_post/made_up_hello").data == "hello from POST" def test_extending_api_with_methods_in_different_modules(): """Test to ensure it's possible to extend the current API with HTTP methods for a view in different modules""" - @hug.extend_api(base_url='/get_and_post') + + @hug.extend_api(base_url="/get_and_post") def extend_with(): import tests.module_fake_simple, tests.module_fake_post - return (tests.module_fake_simple, tests.module_fake_post,) - assert hug.test.get(api, '/get_and_post/made_up_hello').data == 'hello' - assert hug.test.post(api, '/get_and_post/made_up_hello').data == 'hello from POST' + return (tests.module_fake_simple, tests.module_fake_post) + + assert hug.test.get(api, "/get_and_post/made_up_hello").data == "hello" + assert hug.test.post(api, "/get_and_post/made_up_hello").data == "hello from POST" def test_extending_api_with_http_and_cli(): """Test to ensure it's possible to extend the current API so both HTTP and CLI APIs are extended""" import tests.module_fake_http_and_cli - @hug.extend_api(base_url='/api') + @hug.extend_api(base_url="/api") def extend_with(): - return (tests.module_fake_http_and_cli, ) + return (tests.module_fake_http_and_cli,) - assert hug.test.get(api, '/api/made_up_go').data == 'Going!' - assert tests.module_fake_http_and_cli.made_up_go() == 'Going!' - assert hug.test.cli('made_up_go', api=api) + assert hug.test.get(api, "/api/made_up_go").data == "Going!" + assert tests.module_fake_http_and_cli.made_up_go() == "Going!" + assert hug.test.cli("made_up_go", api=api) # Should be able to apply a prefix when extending CLI APIs - @hug.extend_api(command_prefix='prefix_', http=False) + @hug.extend_api(command_prefix="prefix_", http=False) def extend_with(): - return (tests.module_fake_http_and_cli, ) + return (tests.module_fake_http_and_cli,) - assert hug.test.cli('prefix_made_up_go', api=api) + assert hug.test.cli("prefix_made_up_go", api=api) # OR provide a sub command use to reference the commands - @hug.extend_api(sub_command='sub_api', http=False) + @hug.extend_api(sub_command="sub_api", http=False) def extend_with(): - return (tests.module_fake_http_and_cli, ) + return (tests.module_fake_http_and_cli,) - assert hug.test.cli('sub_api', 'made_up_go', api=api) + assert hug.test.cli("sub_api", "made_up_go", api=api) # But not both with pytest.raises(ValueError): - @hug.extend_api(sub_command='sub_api', command_prefix='api_', http=False) + + @hug.extend_api(sub_command="sub_api", command_prefix="api_", http=False) def extend_with(): - return (tests.module_fake_http_and_cli, ) + return (tests.module_fake_http_and_cli,) def test_extending_api_with_http_and_cli(): """Test to ensure it's possible to extend the current API so both HTTP and CLI APIs are extended""" import tests.module_fake_http_and_cli - @hug.extend_api(base_url='/api') + @hug.extend_api(base_url="/api") def extend_with(): - return (tests.module_fake_http_and_cli, ) + return (tests.module_fake_http_and_cli,) - assert hug.test.get(api, '/api/made_up_go').data == 'Going!' - assert tests.module_fake_http_and_cli.made_up_go() == 'Going!' - assert hug.test.cli('made_up_go', api=api) + assert hug.test.get(api, "/api/made_up_go").data == "Going!" + assert tests.module_fake_http_and_cli.made_up_go() == "Going!" + assert hug.test.cli("made_up_go", api=api) def test_cli(): """Test to ensure the CLI wrapper works as intended""" - @hug.cli('command', '1.0.0', output=str) + + @hug.cli("command", "1.0.0", output=str) def cli_command(name: str, value: int): return (name, value) - assert cli_command('Testing', 1) == ('Testing', 1) - assert hug.test.cli(cli_command, "Bob", 5) == ('Bob', 5) + assert cli_command("Testing", 1) == ("Testing", 1) + assert hug.test.cli(cli_command, "Bob", 5) == ("Bob", 5) def test_cli_requires(): """Test to ensure your can add requirements to a CLI""" + def requires_fail(**kwargs): - return {'requirements': 'not met'} + return {"requirements": "not met"} @hug.cli(output=str, requires=requires_fail) def cli_command(name: str, value: int): return (name, value) - assert cli_command('Testing', 1) == ('Testing', 1) - assert hug.test.cli(cli_command, 'Testing', 1) == {'requirements': 'not met'} + assert cli_command("Testing", 1) == ("Testing", 1) + assert hug.test.cli(cli_command, "Testing", 1) == {"requirements": "not met"} def test_cli_validation(): """Test to ensure your can add custom validation to a CLI""" + def contains_either(fields): - if not fields.get('name', '') and not fields.get('value', 0): - return {'name': 'must be defined', 'value': 'must be defined'} + if not fields.get("name", "") and not fields.get("value", 0): + return {"name": "must be defined", "value": "must be defined"} @hug.cli(output=str, validate=contains_either) - def cli_command(name: str="", value: int=0): + def cli_command(name: str = "", value: int = 0): return (name, value) - assert cli_command('Testing', 1) == ('Testing', 1) - assert hug.test.cli(cli_command) == {'name': 'must be defined', 'value': 'must be defined'} - assert hug.test.cli(cli_command, name='Testing') == ('Testing', 0) + assert cli_command("Testing", 1) == ("Testing", 1) + assert hug.test.cli(cli_command) == {"name": "must be defined", "value": "must be defined"} + assert hug.test.cli(cli_command, name="Testing") == ("Testing", 0) def test_cli_with_defaults(): """Test to ensure CLIs work correctly with default values""" + @hug.cli() - def happy(name: str, age: int, birthday: bool=False): + def happy(name: str, age: int, birthday: bool = False): if birthday: return "Happy {age} birthday {name}!".format(**locals()) else: return "{name} is {age} years old".format(**locals()) - assert happy('Hug', 1) == "Hug is 1 years old" - assert happy('Hug', 1, True) == "Happy 1 birthday Hug!" + assert happy("Hug", 1) == "Hug is 1 years old" + assert happy("Hug", 1, True) == "Happy 1 birthday Hug!" assert hug.test.cli(happy, "Bob", 5) == "Bob is 5 years old" assert hug.test.cli(happy, "Bob", 5, birthday=True) == "Happy 5 birthday Bob!" def test_cli_with_hug_types(): """Test to ensure CLIs work as expected when using hug types""" + @hug.cli() - def happy(name: hug.types.text, age: hug.types.number, birthday: hug.types.boolean=False): + def happy(name: hug.types.text, age: hug.types.number, birthday: hug.types.boolean = False): if birthday: return "Happy {age} birthday {name}!".format(**locals()) else: return "{name} is {age} years old".format(**locals()) - assert happy('Hug', 1) == "Hug is 1 years old" - assert happy('Hug', 1, True) == "Happy 1 birthday Hug!" + assert happy("Hug", 1) == "Hug is 1 years old" + assert happy("Hug", 1, True) == "Happy 1 birthday Hug!" assert hug.test.cli(happy, "Bob", 5) == "Bob is 5 years old" assert hug.test.cli(happy, "Bob", 5, birthday=True) == "Happy 5 birthday Bob!" @hug.cli() - def succeed(success: hug.types.smart_boolean=False): + def succeed(success: hug.types.smart_boolean = False): if success: - return 'Yes!' + return "Yes!" else: - return 'No :(' + return "No :(" - assert hug.test.cli(succeed) == 'No :(' - assert hug.test.cli(succeed, success=True) == 'Yes!' - assert 'succeed' in str(__hug__.cli) + assert hug.test.cli(succeed) == "No :(" + assert hug.test.cli(succeed, success=True) == "Yes!" + assert "succeed" in str(__hug__.cli) @hug.cli() - def succeed(success: hug.types.smart_boolean=True): + def succeed(success: hug.types.smart_boolean = True): if success: - return 'Yes!' + return "Yes!" else: - return 'No :(' + return "No :(" - assert hug.test.cli(succeed) == 'Yes!' - assert hug.test.cli(succeed, success='false') == 'No :(' + assert hug.test.cli(succeed) == "Yes!" + assert hug.test.cli(succeed, success="false") == "No :(" @hug.cli() - def all_the(types: hug.types.multiple=[]): - return types or ['nothing_here'] + def all_the(types: hug.types.multiple = []): + return types or ["nothing_here"] - assert hug.test.cli(all_the) == ['nothing_here'] - assert hug.test.cli(all_the, types=('one', 'two', 'three')) == ['one', 'two', 'three'] + assert hug.test.cli(all_the) == ["nothing_here"] + assert hug.test.cli(all_the, types=("one", "two", "three")) == ["one", "two", "three"] @hug.cli() def all_the(types: hug.types.multiple): - return types or ['nothing_here'] + return types or ["nothing_here"] - assert hug.test.cli(all_the) == ['nothing_here'] - assert hug.test.cli(all_the, 'one', 'two', 'three') == ['one', 'two', 'three'] + assert hug.test.cli(all_the) == ["nothing_here"] + assert hug.test.cli(all_the, "one", "two", "three") == ["one", "two", "three"] @hug.cli() - def one_of(value: hug.types.one_of(['one', 'two'])='one'): + def one_of(value: hug.types.one_of(["one", "two"]) = "one"): return value - assert hug.test.cli(one_of, value='one') == 'one' - assert hug.test.cli(one_of, value='two') == 'two' + assert hug.test.cli(one_of, value="one") == "one" + assert hug.test.cli(one_of, value="two") == "two" def test_cli_with_conflicting_short_options(): """Test to ensure that it's possible to expose a CLI with the same first few letters in option""" + @hug.cli() def test(abe1="Value", abe2="Value2", helper=None): return (abe1, abe2) - assert test() == ('Value', 'Value2') - assert test('hi', 'there') == ('hi', 'there') - assert hug.test.cli(test) == ('Value', 'Value2') - assert hug.test.cli(test, abe1='hi', abe2='there') == ('hi', 'there') + assert test() == ("Value", "Value2") + assert test("hi", "there") == ("hi", "there") + assert hug.test.cli(test) == ("Value", "Value2") + assert hug.test.cli(test, abe1="hi", abe2="there") == ("hi", "there") def test_cli_with_directives(): """Test to ensure it's possible to use directives with hug CLIs""" + @hug.cli() @hug.local() def test(hug_timer): @@ -1117,10 +1207,8 @@ def test(hug_timer): def test_cli_with_class_directives(): - @hug.directive() class ClassDirective(object): - def __init__(self, *args, **kwargs): self.test = 1 @@ -1138,7 +1226,6 @@ class TestObject(object): @hug.directive() class ClassDirectiveWithCleanUp(object): - def __init__(self, *args, **kwargs): self.test_object = TestObject @@ -1181,6 +1268,7 @@ def test_with_attribute_error(class_directive: ClassDirectiveWithCleanUp): def test_cli_with_named_directives(): """Test to ensure you can pass named directives into the cli""" + @hug.cli() @hug.local() def test(timer: hug.directives.Timer): @@ -1193,17 +1281,17 @@ def test(timer: hug.directives.Timer): def test_cli_with_output_transform(): """Test to ensure it's possible to use output transforms with hug CLIs""" + @hug.cli() def test() -> int: - return '5' + return "5" assert isinstance(test(), str) assert isinstance(hug.test.cli(test), int) - @hug.cli(transform=int) def test(): - return '5' + return "5" assert isinstance(test(), str) assert isinstance(hug.test.cli(test), int) @@ -1211,64 +1299,69 @@ def test(): def test_cli_with_short_short_options(): """Test to ensure that it's possible to expose a CLI with 2 very short and similar options""" + @hug.cli() def test(a1="Value", a2="Value2"): return (a1, a2) - assert test() == ('Value', 'Value2') - assert test('hi', 'there') == ('hi', 'there') - assert hug.test.cli(test) == ('Value', 'Value2') - assert hug.test.cli(test, a1='hi', a2='there') == ('hi', 'there') + assert test() == ("Value", "Value2") + assert test("hi", "there") == ("hi", "there") + assert hug.test.cli(test) == ("Value", "Value2") + assert hug.test.cli(test, a1="hi", a2="there") == ("hi", "there") def test_cli_file_return(): """Test to ensure that its possible to return a file stream from a CLI""" + @hug.cli() def test(): - return open(os.path.join(BASE_DIRECTORY, 'README.md'), 'rb') + return open(os.path.join(BASE_DIRECTORY, "README.md"), "rb") - assert 'hug' in hug.test.cli(test) + assert "hug" in hug.test.cli(test) def test_local_type_annotation(): """Test to ensure local type annotation works as expected""" + @hug.local(raise_on_invalid=True) def test(number: int): return number assert test(3) == 3 with pytest.raises(Exception): - test('h') + test("h") @hug.local(raise_on_invalid=False) def test(number: int): return number - assert test('h')['errors'] + assert test("h")["errors"] @hug.local(raise_on_invalid=False, validate=False) def test(number: int): return number - assert test('h') == 'h' + assert test("h") == "h" def test_local_transform(): """Test to ensure local type annotation works as expected""" + @hug.local(transform=str) def test(number: int): return number - assert test(3) == '3' + assert test(3) == "3" def test_local_on_invalid(): """Test to ensure local type annotation works as expected""" + @hug.local(on_invalid=str) def test(number: int): return number - assert isinstance(test('h'), str) + assert isinstance(test("h"), str) def test_local_requires(): @@ -1276,59 +1369,68 @@ def test_local_requires(): global_state = False def requirement(**kwargs): - return global_state and 'Unauthorized' + return global_state and "Unauthorized" @hug.local(requires=requirement) def hello(): - return 'Hi!' + return "Hi!" - assert hello() == 'Hi!' + assert hello() == "Hi!" global_state = True - assert hello() == 'Unauthorized' + assert hello() == "Unauthorized" def test_static_file_support(): """Test to ensure static file routing works as expected""" - @hug.static('/static') + + @hug.static("/static") def my_static_dirs(): - return (BASE_DIRECTORY, ) + return (BASE_DIRECTORY,) - assert 'hug' in hug.test.get(api, '/static/README.md').data - assert 'Index' in hug.test.get(api, '/static/tests/data').data - assert '404' in hug.test.get(api, '/static/NOT_IN_EXISTANCE.md').status + assert "hug" in hug.test.get(api, "/static/README.md").data + assert "Index" in hug.test.get(api, "/static/tests/data").data + assert "404" in hug.test.get(api, "/static/NOT_IN_EXISTANCE.md").status def test_static_jailed(): """Test to ensure we can't serve from outside static dir""" - @hug.static('/static') + + @hug.static("/static") def my_static_dirs(): - return ['tests'] - assert '404' in hug.test.get(api, '/static/../README.md').status + return ["tests"] + + assert "404" in hug.test.get(api, "/static/../README.md").status -@pytest.mark.skipif(sys.platform == 'win32', reason='Currently failing on Windows build') +@pytest.mark.skipif(sys.platform == "win32", reason="Currently failing on Windows build") def test_sink_support(): """Test to ensure sink URL routers work as expected""" - @hug.sink('/all') + + @hug.sink("/all") def my_sink(request): - return request.path.replace('/all', '') + return request.path.replace("/all", "") + + assert hug.test.get(api, "/all/the/things").data == "/the/things" - assert hug.test.get(api, '/all/the/things').data == '/the/things' -@pytest.mark.skipif(sys.platform == 'win32', reason='Currently failing on Windows build') +@pytest.mark.skipif(sys.platform == "win32", reason="Currently failing on Windows build") def test_sink_support_with_base_url(): """Test to ensure sink URL routers work when the API is extended with a specified base URL""" - @hug.extend_api('/fake', base_url='/api') + + @hug.extend_api("/fake", base_url="/api") def extend_with(): import tests.module_fake - return (tests.module_fake, ) - assert hug.test.get(api, '/api/fake/all/the/things').data == '/the/things' + return (tests.module_fake,) + + assert hug.test.get(api, "/api/fake/all/the/things").data == "/the/things" + def test_cli_with_string_annotation(): """Test to ensure CLI's work correctly with string annotations""" + @hug.cli() - def test(value_1: 'The first value', value_2: 'The second value'=None): + def test(value_1: "The first value", value_2: "The second value" = None): return True assert hug.test.cli(test, True) @@ -1336,91 +1438,98 @@ def test(value_1: 'The first value', value_2: 'The second value'=None): def test_cli_with_args(): """Test to ensure CLI's work correctly when taking args""" + @hug.cli() def test(*values): return values assert test(1, 2, 3) == (1, 2, 3) - assert hug.test.cli(test, 1, 2, 3) == ('1', '2', '3') + assert hug.test.cli(test, 1, 2, 3) == ("1", "2", "3") def test_cli_using_method(): """Test to ensure that attaching a cli to a class method works as expected""" - class API(object): + class API(object): def __init__(self): hug.cli()(self.hello_world_method) def hello_world_method(self): - variable = 'Hello World!' + variable = "Hello World!" return variable api_instance = API() - assert api_instance.hello_world_method() == 'Hello World!' - assert hug.test.cli(api_instance.hello_world_method) == 'Hello World!' + assert api_instance.hello_world_method() == "Hello World!" + assert hug.test.cli(api_instance.hello_world_method) == "Hello World!" assert hug.test.cli(api_instance.hello_world_method, collect_output=False) is None def test_cli_with_nested_variables(): """Test to ensure that a cli containing multiple nested variables works correctly""" + @hug.cli() def test(value_1=None, value_2=None): - return 'Hi!' + return "Hi!" - assert hug.test.cli(test) == 'Hi!' + assert hug.test.cli(test) == "Hi!" def test_cli_with_exception(): """Test to ensure that a cli with an exception is correctly handled""" + @hug.cli() def test(): raise ValueError() - return 'Hi!' + return "Hi!" - assert hug.test.cli(test) != 'Hi!' + assert hug.test.cli(test) != "Hi!" -@pytest.mark.skipif(sys.platform == 'win32', reason='Currently failing on Windows build') +@pytest.mark.skipif(sys.platform == "win32", reason="Currently failing on Windows build") def test_wraps(): """Test to ensure you can safely apply decorators to hug endpoints by using @hug.wraps""" + def my_decorator(function): @hug.wraps(function) def decorated(*args, **kwargs): - kwargs['name'] = 'Timothy' + kwargs["name"] = "Timothy" return function(*args, **kwargs) + return decorated @hug.get() @my_decorator def what_is_my_name(hug_timer=None, name="Sam"): - return {'name': name, 'took': hug_timer} + return {"name": name, "took": hug_timer} - result = hug.test.get(api, 'what_is_my_name').data - assert result['name'] == 'Timothy' - assert result['took'] + result = hug.test.get(api, "what_is_my_name").data + assert result["name"] == "Timothy" + assert result["took"] def my_second_decorator(function): @hug.wraps(function) def decorated(*args, **kwargs): - kwargs['name'] = "Not telling" + kwargs["name"] = "Not telling" return function(*args, **kwargs) + return decorated @hug.get() @my_decorator @my_second_decorator def what_is_my_name2(hug_timer=None, name="Sam"): - return {'name': name, 'took': hug_timer} + return {"name": name, "took": hug_timer} - result = hug.test.get(api, 'what_is_my_name2').data - assert result['name'] == "Not telling" - assert result['took'] + result = hug.test.get(api, "what_is_my_name2").data + assert result["name"] == "Not telling" + assert result["took"] def my_decorator_with_request(function): @hug.wraps(function) def decorated(request, *args, **kwargs): - kwargs['has_request'] = bool(request) + kwargs["has_request"] = bool(request) return function(*args, **kwargs) + return decorated @hug.get() @@ -1428,11 +1537,12 @@ def decorated(request, *args, **kwargs): def do_you_have_request(has_request=False): return has_request - assert hug.test.get(api, 'do_you_have_request').data + assert hug.test.get(api, "do_you_have_request").data def test_cli_with_empty_return(): """Test to ensure that if you return None no data will be added to sys.stdout""" + @hug.cli() def test_empty_return(): pass @@ -1442,12 +1552,12 @@ def test_empty_return(): def test_cli_with_multiple_ints(): """Test to ensure multiple ints work with CLI""" + @hug.cli() def test_multiple_cli(ints: hug.types.comma_separated_list): return ints - assert hug.test.cli(test_multiple_cli, ints='1,2,3') == ['1', '2', '3'] - + assert hug.test.cli(test_multiple_cli, ints="1,2,3") == ["1", "2", "3"] class ListOfInts(hug.types.Multiple): """Only accept a list of numbers.""" @@ -1457,20 +1567,21 @@ def __call__(self, value): return [int(number) for number in value] @hug.cli() - def test_multiple_cli(ints: ListOfInts()=[]): + def test_multiple_cli(ints: ListOfInts() = []): return ints - assert hug.test.cli(test_multiple_cli, ints=['1', '2', '3']) == [1, 2, 3] + assert hug.test.cli(test_multiple_cli, ints=["1", "2", "3"]) == [1, 2, 3] @hug.cli() - def test_multiple_cli(ints: hug.types.Multiple[int]()=[]): + def test_multiple_cli(ints: hug.types.Multiple[int]() = []): return ints - assert hug.test.cli(test_multiple_cli, ints=['1', '2', '3']) == [1, 2, 3] + assert hug.test.cli(test_multiple_cli, ints=["1", "2", "3"]) == [1, 2, 3] def test_startup(): """Test to ensure hug startup decorators work as expected""" + @hug.startup() def happens_on_startup(api): pass @@ -1484,32 +1595,33 @@ def async_happens_on_startup(api): assert async_happens_on_startup in api.startup_handlers -@pytest.mark.skipif(sys.platform == 'win32', reason='Currently failing on Windows build') +@pytest.mark.skipif(sys.platform == "win32", reason="Currently failing on Windows build") def test_adding_headers(): """Test to ensure it is possible to inject response headers based on only the URL route""" - @hug.get(response_headers={'name': 'Timothy'}) + + @hug.get(response_headers={"name": "Timothy"}) def endpoint(): - return '' + return "" - result = hug.test.get(api, 'endpoint') - assert result.data == '' - assert result.headers_dict['name'] == 'Timothy' + result = hug.test.get(api, "endpoint") + assert result.data == "" + assert result.headers_dict["name"] == "Timothy" def test_on_demand_404(hug_api): """Test to ensure it's possible to route to a 404 response on demand""" + @hug_api.route.http.get() def my_endpoint(hug_api): return hug_api.http.not_found - assert '404' in hug.test.get(hug_api, 'my_endpoint').status - + assert "404" in hug.test.get(hug_api, "my_endpoint").status @hug_api.route.http.get() def my_endpoint2(hug_api): raise hug.HTTPNotFound() - assert '404' in hug.test.get(hug_api, 'my_endpoint2').status + assert "404" in hug.test.get(hug_api, "my_endpoint2").status @hug_api.route.http.get() def my_endpoint3(hug_api): @@ -1517,68 +1629,73 @@ def my_endpoint3(hug_api): del hug_api.http._not_found return hug_api.http.not_found - assert '404' in hug.test.get(hug_api, 'my_endpoint3').status + assert "404" in hug.test.get(hug_api, "my_endpoint3").status -@pytest.mark.skipif(sys.platform == 'win32', reason='Currently failing on Windows build') +@pytest.mark.skipif(sys.platform == "win32", reason="Currently failing on Windows build") def test_exceptions(): """Test to ensure hug's exception handling decorator works as expected""" + @hug.get() def endpoint(): - raise ValueError('hi') + raise ValueError("hi") with pytest.raises(ValueError): - hug.test.get(api, 'endpoint') + hug.test.get(api, "endpoint") @hug.exception() def handle_exception(exception): - return 'it worked' + return "it worked" - assert hug.test.get(api, 'endpoint').data == 'it worked' + assert hug.test.get(api, "endpoint").data == "it worked" @hug.exception(ValueError) # noqa def handle_exception(exception): - return 'more explicit handler also worked' + return "more explicit handler also worked" - assert hug.test.get(api, 'endpoint').data == 'more explicit handler also worked' + assert hug.test.get(api, "endpoint").data == "more explicit handler also worked" -@pytest.mark.skipif(sys.platform == 'win32', reason='Currently failing on Windows build') +@pytest.mark.skipif(sys.platform == "win32", reason="Currently failing on Windows build") def test_validate(): """Test to ensure hug's secondary validation mechanism works as expected""" + def contains_either(fields): - if not 'one' in fields and not 'two' in fields: - return {'one': 'must be defined', 'two': 'must be defined'} + if not "one" in fields and not "two" in fields: + return {"one": "must be defined", "two": "must be defined"} @hug.get(validate=contains_either) def my_endpoint(one=None, two=None): return True - - assert hug.test.get(api, 'my_endpoint', one=True).data - assert hug.test.get(api, 'my_endpoint', two=True).data - assert hug.test.get(api, 'my_endpoint').status - assert hug.test.get(api, 'my_endpoint').data == {'errors': {'one': 'must be defined', 'two': 'must be defined'}} + assert hug.test.get(api, "my_endpoint", one=True).data + assert hug.test.get(api, "my_endpoint", two=True).data + assert hug.test.get(api, "my_endpoint").status + assert hug.test.get(api, "my_endpoint").data == { + "errors": {"one": "must be defined", "two": "must be defined"} + } def test_cli_api(capsys): """Ensure that the overall CLI Interface API works as expected""" + @hug.cli() def my_cli_command(): print("Success!") - with mock.patch('sys.argv', ['/bin/command', 'my_cli_command']): + with mock.patch("sys.argv", ["/bin/command", "my_cli_command"]): __hug__.cli() out, err = capsys.readouterr() assert "Success!" in out - with mock.patch('sys.argv', []): + with mock.patch("sys.argv", []): with pytest.raises(SystemExit): __hug__.cli() def test_cli_api_return(): """Ensure returning from a CLI API works as expected""" + @hug.cli() def my_cli_command(): return "Success!" @@ -1588,95 +1705,133 @@ def my_cli_command(): def test_urlencoded(): """Ensure that urlencoded input format works as intended""" + @hug.post() def test_url_encoded_post(**kwargs): return kwargs - test_data = b'foo=baz&foo=bar&name=John+Doe' - assert hug.test.post(api, 'test_url_encoded_post', body=test_data, headers={'content-type': 'application/x-www-form-urlencoded'}).data == {'name': 'John Doe', 'foo': ['baz', 'bar']} + test_data = b"foo=baz&foo=bar&name=John+Doe" + assert hug.test.post( + api, + "test_url_encoded_post", + body=test_data, + headers={"content-type": "application/x-www-form-urlencoded"}, + ).data == {"name": "John Doe", "foo": ["baz", "bar"]} def test_multipart(): """Ensure that multipart input format works as intended""" + @hug.post() def test_multipart_post(**kwargs): return kwargs - with open(os.path.join(BASE_DIRECTORY, 'artwork', 'logo.png'), 'rb') as logo: - prepared_request = requests.Request('POST', 'http://localhost/', files={'logo': logo}).prepare() + with open(os.path.join(BASE_DIRECTORY, "artwork", "logo.png"), "rb") as logo: + prepared_request = requests.Request( + "POST", "http://localhost/", files={"logo": logo} + ).prepare() logo.seek(0) - output = json.loads(hug.defaults.output_format({'logo': logo.read()}).decode('utf8')) - assert hug.test.post(api, 'test_multipart_post', body=prepared_request.body, - headers=prepared_request.headers).data == output + output = json.loads(hug.defaults.output_format({"logo": logo.read()}).decode("utf8")) + assert ( + hug.test.post( + api, + "test_multipart_post", + body=prepared_request.body, + headers=prepared_request.headers, + ).data + == output + ) def test_json_null(hug_api): """Test to ensure passing in null within JSON will be seen as None and not allowed by text values""" + @hug_api.route.http.post() def test_naive(argument_1): return argument_1 - assert hug.test.post(hug_api, 'test_naive', body='{"argument_1": null}', - headers={'content-type': 'application/json'}).data == None - + assert ( + hug.test.post( + hug_api, + "test_naive", + body='{"argument_1": null}', + headers={"content-type": "application/json"}, + ).data + == None + ) @hug_api.route.http.post() def test_text_type(argument_1: hug.types.text): return argument_1 - - assert 'errors' in hug.test.post(hug_api, 'test_text_type', body='{"argument_1": null}', - headers={'content-type': 'application/json'}).data + assert ( + "errors" + in hug.test.post( + hug_api, + "test_text_type", + body='{"argument_1": null}', + headers={"content-type": "application/json"}, + ).data + ) def test_json_self_key(hug_api): """Test to ensure passing in a json with a key named 'self' works as expected""" + @hug_api.route.http.post() def test_self_post(body): return body - assert hug.test.post(hug_api, 'test_self_post', body='{"self": "this"}', - headers={'content-type': 'application/json'}).data == {"self": "this"} + assert hug.test.post( + hug_api, + "test_self_post", + body='{"self": "this"}', + headers={"content-type": "application/json"}, + ).data == {"self": "this"} def test_204_with_no_body(hug_api): """Test to ensure returning no body on a 204 statused endpoint works without issue""" + @hug_api.route.http.delete() def test_route(response): response.status = hug.HTTP_204 return - assert '204' in hug.test.delete(hug_api, 'test_route').status + assert "204" in hug.test.delete(hug_api, "test_route").status def test_output_format_inclusion(hug_api): """Test to ensure output format can live in one api but apply to the other""" + @hug.get() def my_endpoint(): - return 'hello' + return "hello" @hug.default_output_format(api=hug_api) def mutated_json(data): - return hug.output_format.json({'mutated': data}) + return hug.output_format.json({"mutated": data}) - hug_api.extend(api, '') + hug_api.extend(api, "") - assert hug.test.get(hug_api, 'my_endpoint').data == {'mutated': 'hello'} + assert hug.test.get(hug_api, "my_endpoint").data == {"mutated": "hello"} def test_api_pass_along(hug_api): """Test to ensure the correct API instance is passed along using API directive""" + @hug.get() def takes_api(hug_api): return hug_api.__name__ hug_api.__name__ = "Test API" - hug_api.extend(api, '') - assert hug.test.get(hug_api, 'takes_api').data == hug_api.__name__ + hug_api.extend(api, "") + assert hug.test.get(hug_api, "takes_api").data == hug_api.__name__ def test_exception_excludes(hug_api): """Test to ensure it's possible to add excludes to exception routers""" + class MyValueError(ValueError): pass @@ -1685,11 +1840,11 @@ class MySecondValueError(ValueError): @hug.exception(Exception, exclude=MySecondValueError, api=hug_api) def base_exception_handler(exception): - return 'base exception handler' + return "base exception handler" @hug.exception(ValueError, exclude=(MyValueError, MySecondValueError), api=hug_api) def base_exception_handler(exception): - return 'special exception handler' + return "special exception handler" @hug.get(api=hug_api) def my_handler(): @@ -1697,68 +1852,85 @@ def my_handler(): @hug.get(api=hug_api) def fall_through_handler(): - raise ValueError('reason') + raise ValueError("reason") @hug.get(api=hug_api) def full_through_to_raise(): raise MySecondValueError() - assert hug.test.get(hug_api, 'my_handler').data == 'base exception handler' - assert hug.test.get(hug_api, 'fall_through_handler').data == 'special exception handler' + assert hug.test.get(hug_api, "my_handler").data == "base exception handler" + assert hug.test.get(hug_api, "fall_through_handler").data == "special exception handler" with pytest.raises(MySecondValueError): - assert hug.test.get(hug_api, 'full_through_to_raise').data + assert hug.test.get(hug_api, "full_through_to_raise").data def test_cli_kwargs(hug_api): """Test to ensure cli commands can correctly handle **kwargs""" + @hug.cli(api=hug_api) def takes_all_the_things(required_argument, named_argument=False, *args, **kwargs): return [required_argument, named_argument, args, kwargs] - assert hug.test.cli(takes_all_the_things, 'hi!') == ['hi!', False, (), {}] - assert hug.test.cli(takes_all_the_things, 'hi!', named_argument='there') == ['hi!', 'there', (), {}] - assert hug.test.cli(takes_all_the_things, 'hi!', 'extra', '--arguments', 'can', '--happen', '--all', 'the', 'tim') \ - == ['hi!', False, ('extra', ), {'arguments': 'can', 'happen': True, 'all': ['the', 'tim']}] + assert hug.test.cli(takes_all_the_things, "hi!") == ["hi!", False, (), {}] + assert hug.test.cli(takes_all_the_things, "hi!", named_argument="there") == [ + "hi!", + "there", + (), + {}, + ] + assert hug.test.cli( + takes_all_the_things, + "hi!", + "extra", + "--arguments", + "can", + "--happen", + "--all", + "the", + "tim", + ) == ["hi!", False, ("extra",), {"arguments": "can", "happen": True, "all": ["the", "tim"]}] def test_api_gets_extra_variables_without_kargs_or_kwargs(hug_api): """Test to ensure it's possiible to extra all params without specifying them exactly""" + @hug.get(api=hug_api) def ensure_params(request, response): return request.params - assert hug.test.get(hug_api, 'ensure_params', params={'make': 'it'}).data == {'make': 'it'} - assert hug.test.get(hug_api, 'ensure_params', hello='world').data == {'hello': 'world'} + assert hug.test.get(hug_api, "ensure_params", params={"make": "it"}).data == {"make": "it"} + assert hug.test.get(hug_api, "ensure_params", hello="world").data == {"hello": "world"} def test_utf8_output(hug_api): """Test to ensure unicode data is correct outputed on JSON outputs without modification""" + @hug.get(api=hug_api) def output_unicode(): - return {'data': 'Τη γλώσσα μου έδωσαν ελληνική'} + return {"data": "Τη γλώσσα μου έδωσαν ελληνική"} - assert hug.test.get(hug_api, 'output_unicode').data == {'data': 'Τη γλώσσα μου έδωσαν ελληνική'} + assert hug.test.get(hug_api, "output_unicode").data == {"data": "Τη γλώσσα μου έδωσαν ελληνική"} def test_param_rerouting(hug_api): - @hug.local(api=hug_api, map_params={'local_id': 'record_id'}) - @hug.cli(api=hug_api, map_params={'cli_id': 'record_id'}) - @hug.get(api=hug_api, map_params={'id': 'record_id'}) + @hug.local(api=hug_api, map_params={"local_id": "record_id"}) + @hug.cli(api=hug_api, map_params={"cli_id": "record_id"}) + @hug.get(api=hug_api, map_params={"id": "record_id"}) def pull_record(record_id: hug.types.number): return record_id - assert hug.test.get(hug_api, 'pull_record', id=10).data == 10 - assert hug.test.get(hug_api, 'pull_record', id='10').data == 10 - assert 'errors' in hug.test.get(hug_api, 'pull_record', id='ten').data + assert hug.test.get(hug_api, "pull_record", id=10).data == 10 + assert hug.test.get(hug_api, "pull_record", id="10").data == 10 + assert "errors" in hug.test.get(hug_api, "pull_record", id="ten").data assert hug.test.cli(pull_record, cli_id=10) == 10 - assert hug.test.cli(pull_record, cli_id='10') == 10 + assert hug.test.cli(pull_record, cli_id="10") == 10 with pytest.raises(SystemExit): - hug.test.cli(pull_record, cli_id='ten') + hug.test.cli(pull_record, cli_id="ten") assert pull_record(local_id=10) - @hug.get(api=hug_api, map_params={'id': 'record_id'}) - def pull_record(record_id: hug.types.number=1): + @hug.get(api=hug_api, map_params={"id": "record_id"}) + def pull_record(record_id: hug.types.number = 1): return record_id - assert hug.test.get(hug_api, 'pull_record').data == 1 - assert hug.test.get(hug_api, 'pull_record', id=10).data == 10 + assert hug.test.get(hug_api, "pull_record").data == 1 + assert hug.test.get(hug_api, "pull_record", id=10).data == 10 diff --git a/tests/test_directives.py b/tests/test_directives.py index 40caa288..ca206993 100644 --- a/tests/test_directives.py +++ b/tests/test_directives.py @@ -51,44 +51,48 @@ def test_timer(): def timer_tester(hug_timer): return hug_timer - assert isinstance(hug.test.get(api, 'timer_tester').data, float) + assert isinstance(hug.test.get(api, "timer_tester").data, float) assert isinstance(timer_tester(), hug.directives.Timer) def test_module(): """Test to ensure the module directive automatically includes the current API's module""" + @hug.get() def module_tester(hug_module): return hug_module.__name__ - assert hug.test.get(api, 'module_tester').data == api.module.__name__ + assert hug.test.get(api, "module_tester").data == api.module.__name__ def test_api(): """Ensure the api correctly gets passed onto a hug API function based on a directive""" + @hug.get() def api_tester(hug_api): return hug_api == api - assert hug.test.get(api, 'api_tester').data is True + assert hug.test.get(api, "api_tester").data is True def test_documentation(): """Test documentation directive""" - assert 'handlers' in hug.directives.documentation(api=api) + assert "handlers" in hug.directives.documentation(api=api) def test_api_version(): """Ensure that it's possible to get the current version of an API based on a directive""" + @hug.get(versions=1) def version_tester(hug_api_version): return hug_api_version - assert hug.test.get(api, 'v1/version_tester').data == 1 + assert hug.test.get(api, "v1/version_tester").data == 1 def test_current_api(): """Ensure that it's possible to retrieve methods from the same version of the API""" + @hug.get(versions=1) def first_method(): return "Success" @@ -97,7 +101,7 @@ def first_method(): def version_call_tester(hug_current_api): return hug_current_api.first_method() - assert hug.test.get(api, 'v1/version_call_tester').data == 'Success' + assert hug.test.get(api, "v1/version_call_tester").data == "Success" @hug.get() def second_method(): @@ -107,34 +111,40 @@ def second_method(): def version_call_tester(hug_current_api): return hug_current_api.second_method() - assert hug.test.get(api, 'v2/version_call_tester').data == 'Unversioned' + assert hug.test.get(api, "v2/version_call_tester").data == "Unversioned" @hug.get(versions=3) # noqa def version_call_tester(hug_current_api): return hug_current_api.first_method() with pytest.raises(AttributeError): - hug.test.get(api, 'v3/version_call_tester').data + hug.test.get(api, "v3/version_call_tester").data def test_user(): """Ensure that it's possible to get the current authenticated user based on a directive""" - user = 'test_user' - password = 'super_secret' + user = "test_user" + password = "super_secret" @hug.get(requires=hug.authentication.basic(hug.authentication.verify(user, password))) def authenticated_hello(hug_user): return hug_user - token = b64encode('{0}:{1}'.format(user, password).encode('utf8')).decode('utf8') - assert hug.test.get(api, 'authenticated_hello', headers={'Authorization': 'Basic {0}'.format(token)}).data == user + token = b64encode("{0}:{1}".format(user, password).encode("utf8")).decode("utf8") + assert ( + hug.test.get( + api, "authenticated_hello", headers={"Authorization": "Basic {0}".format(token)} + ).data + == user + ) def test_session_directive(): """Ensure that it's possible to retrieve the session withing a request using the built-in session directive""" + @hug.request_middleware() def add_session(request, response): - request.context['session'] = {'test': 'data'} + request.context["session"] = {"test": "data"} @hug.local() @hug.get() @@ -142,13 +152,14 @@ def session_data(hug_session): return hug_session assert session_data() is None - assert hug.test.get(api, 'session_data').data == {'test': 'data'} + assert hug.test.get(api, "session_data").data == {"test": "data"} def test_named_directives(): """Ensure that it's possible to attach directives to named parameters""" + @hug.get() - def test(time: hug.directives.Timer=3): + def test(time: hug.directives.Timer = 3): return time assert isinstance(test(1), int) @@ -159,14 +170,15 @@ def test(time: hug.directives.Timer=3): def test_local_named_directives(): """Ensure that it's possible to attach directives to local function calling""" + @hug.local() - def test(time: __hug__.directive('timer')=3): + def test(time: __hug__.directive("timer") = 3): return time assert isinstance(test(), hug.directives.Timer) @hug.local(directives=False) - def test(time: __hug__.directive('timer')=3): + def test(time: __hug__.directive("timer") = 3): return time assert isinstance(test(3), int) @@ -174,9 +186,10 @@ def test(time: __hug__.directive('timer')=3): def test_named_directives_by_name(): """Ensure that it's possible to attach directives to named parameters using only the name of the directive""" + @hug.get() @hug.local() - def test(time: __hug__.directive('timer')=3): + def test(time: __hug__.directive("timer") = 3): return time assert isinstance(test(), hug.directives.Timer) @@ -184,39 +197,45 @@ def test(time: __hug__.directive('timer')=3): def test_per_api_directives(): """Test to ensure it's easy to define a directive within an API""" + @hug.directive(apply_globally=False) def test(default=None, **kwargs): return default @hug.get() - def my_api_method(hug_test='heyyy'): + def my_api_method(hug_test="heyyy"): return hug_test - assert hug.test.get(api, 'my_api_method').data == 'heyyy' + assert hug.test.get(api, "my_api_method").data == "heyyy" def test_user_directives(): """Test the user directives functionality, to ensure it will provide the set user object""" + @hug.get() # noqa def try_user(user: hug.directives.user): return user - assert hug.test.get(api, 'try_user').data is None + assert hug.test.get(api, "try_user").data is None - @hug.get(requires=hug.authentication.basic(hug.authentication.verify('Tim', 'Custom password'))) # noqa + @hug.get( + requires=hug.authentication.basic(hug.authentication.verify("Tim", "Custom password")) + ) # noqa def try_user(user: hug.directives.user): return user - token = b'Basic ' + b64encode('{0}:{1}'.format('Tim', 'Custom password').encode('utf8')) - assert hug.test.get(api, 'try_user', headers={'Authorization': token}).data == 'Tim' + token = b"Basic " + b64encode("{0}:{1}".format("Tim", "Custom password").encode("utf8")) + assert hug.test.get(api, "try_user", headers={"Authorization": token}).data == "Tim" def test_directives(hug_api): """Test to ensure cors directive works as expected""" - assert hug.directives.cors('google.com') == 'google.com' + assert hug.directives.cors("google.com") == "google.com" @hug.get(api=hug_api) - def cors_supported(cors: hug.directives.cors="*"): + def cors_supported(cors: hug.directives.cors = "*"): return True - assert hug.test.get(hug_api, 'cors_supported').headers_dict['Access-Control-Allow-Origin'] == '*' + assert ( + hug.test.get(hug_api, "cors_supported").headers_dict["Access-Control-Allow-Origin"] == "*" + ) diff --git a/tests/test_documentation.py b/tests/test_documentation.py index ed36e433..0655f1ed 100644 --- a/tests/test_documentation.py +++ b/tests/test_documentation.py @@ -33,6 +33,7 @@ def test_basic_documentation(): """Ensure creating and then documenting APIs with Hug works as intuitively as expected""" + @hug.get() def hello_world(): """Returns hello world""" @@ -43,8 +44,8 @@ def echo(text): """Returns back whatever data it is given in the text parameter""" return text - @hug.post('/happy_birthday', examples="name=HUG&age=1") - def birthday(name, age: hug.types.number=1): + @hug.post("/happy_birthday", examples="name=HUG&age=1") + def birthday(name, age: hug.types.number = 1): """Says happy birthday to a user""" return "Happy {age} Birthday {name}!".format(**locals()) @@ -54,7 +55,7 @@ def noop(request, response): pass @hug.get() - def string_docs(data: 'Takes data', ignore_directive: hug.directives.Timer) -> 'Returns data': + def string_docs(data: "Takes data", ignore_directive: hug.directives.Timer) -> "Returns data": """Annotations defined with strings should be documentation only""" pass @@ -64,45 +65,47 @@ def private(): pass documentation = api.http.documentation() - assert 'test_documentation' in documentation['overview'] - - assert '/hello_world' in documentation['handlers'] - assert '/echo' in documentation['handlers'] - assert '/happy_birthday' in documentation['handlers'] - assert '/birthday' not in documentation['handlers'] - assert '/noop' in documentation['handlers'] - assert '/string_docs' in documentation['handlers'] - assert '/private' not in documentation['handlers'] - - assert documentation['handlers']['/hello_world']['GET']['usage'] == "Returns hello world" - assert documentation['handlers']['/hello_world']['GET']['examples'] == ["/hello_world"] - assert documentation['handlers']['/hello_world']['GET']['outputs']['content_type'] in [ + assert "test_documentation" in documentation["overview"] + + assert "/hello_world" in documentation["handlers"] + assert "/echo" in documentation["handlers"] + assert "/happy_birthday" in documentation["handlers"] + assert "/birthday" not in documentation["handlers"] + assert "/noop" in documentation["handlers"] + assert "/string_docs" in documentation["handlers"] + assert "/private" not in documentation["handlers"] + + assert documentation["handlers"]["/hello_world"]["GET"]["usage"] == "Returns hello world" + assert documentation["handlers"]["/hello_world"]["GET"]["examples"] == ["/hello_world"] + assert documentation["handlers"]["/hello_world"]["GET"]["outputs"]["content_type"] in [ "application/json", - "application/json; charset=utf-8" + "application/json; charset=utf-8", ] - assert 'inputs' not in documentation['handlers']['/hello_world']['GET'] + assert "inputs" not in documentation["handlers"]["/hello_world"]["GET"] - assert 'text' in documentation['handlers']['/echo']['POST']['inputs']['text']['type'] - assert 'default' not in documentation['handlers']['/echo']['POST']['inputs']['text'] + assert "text" in documentation["handlers"]["/echo"]["POST"]["inputs"]["text"]["type"] + assert "default" not in documentation["handlers"]["/echo"]["POST"]["inputs"]["text"] - assert 'number' in documentation['handlers']['/happy_birthday']['POST']['inputs']['age']['type'] - assert documentation['handlers']['/happy_birthday']['POST']['inputs']['age']['default'] == 1 + assert "number" in documentation["handlers"]["/happy_birthday"]["POST"]["inputs"]["age"]["type"] + assert documentation["handlers"]["/happy_birthday"]["POST"]["inputs"]["age"]["default"] == 1 - assert 'inputs' not in documentation['handlers']['/noop']['POST'] + assert "inputs" not in documentation["handlers"]["/noop"]["POST"] - assert documentation['handlers']['/string_docs']['GET']['inputs']['data']['type'] == 'Takes data' - assert documentation['handlers']['/string_docs']['GET']['outputs']['type'] == 'Returns data' - assert 'ignore_directive' not in documentation['handlers']['/string_docs']['GET']['inputs'] + assert ( + documentation["handlers"]["/string_docs"]["GET"]["inputs"]["data"]["type"] == "Takes data" + ) + assert documentation["handlers"]["/string_docs"]["GET"]["outputs"]["type"] == "Returns data" + assert "ignore_directive" not in documentation["handlers"]["/string_docs"]["GET"]["inputs"] @hug.post(versions=1) # noqa def echo(text): """V1 Docs""" - return 'V1' + return "V1" @hug.post(versions=2) # noqa def echo(text): """V1 Docs""" - return 'V2' + return "V2" @hug.post(versions=2) def test(text): @@ -111,64 +114,67 @@ def test(text): @hug.get(requires=test) def unversioned(): - return 'Hello' + return "Hello" @hug.get(versions=False) def noversions(): pass - @hug.extend_api('/fake', base_url='/api') + @hug.extend_api("/fake", base_url="/api") def extend_with(): import tests.module_fake_simple - return (tests.module_fake_simple, ) + + return (tests.module_fake_simple,) versioned_doc = api.http.documentation() - assert 'versions' in versioned_doc - assert 1 in versioned_doc['versions'] - assert 2 in versioned_doc['versions'] - assert False not in versioned_doc['versions'] - assert '/unversioned' in versioned_doc['handlers'] - assert '/echo' in versioned_doc['handlers'] - assert '/test' in versioned_doc['handlers'] + assert "versions" in versioned_doc + assert 1 in versioned_doc["versions"] + assert 2 in versioned_doc["versions"] + assert False not in versioned_doc["versions"] + assert "/unversioned" in versioned_doc["handlers"] + assert "/echo" in versioned_doc["handlers"] + assert "/test" in versioned_doc["handlers"] specific_version_doc = api.http.documentation(api_version=1) - assert 'versions' in specific_version_doc - assert '/echo' in specific_version_doc['handlers'] - assert '/unversioned' in specific_version_doc['handlers'] - assert specific_version_doc['handlers']['/unversioned']['GET']['requires'] == ['V1 Docs'] - assert '/test' not in specific_version_doc['handlers'] + assert "versions" in specific_version_doc + assert "/echo" in specific_version_doc["handlers"] + assert "/unversioned" in specific_version_doc["handlers"] + assert specific_version_doc["handlers"]["/unversioned"]["GET"]["requires"] == ["V1 Docs"] + assert "/test" not in specific_version_doc["handlers"] - specific_base_doc = api.http.documentation(base_url='/api') - assert '/echo' not in specific_base_doc['handlers'] - assert '/fake/made_up_hello' in specific_base_doc['handlers'] + specific_base_doc = api.http.documentation(base_url="/api") + assert "/echo" not in specific_base_doc["handlers"] + assert "/fake/made_up_hello" in specific_base_doc["handlers"] handler = api.http.documentation_404() response = StartResponseMock() - handler(Request(create_environ(path='v1/doc')), response) - documentation = json.loads(response.data.decode('utf8'))['documentation'] - assert 'versions' in documentation - assert '/echo' in documentation['handlers'] - assert '/test' not in documentation['handlers'] + handler(Request(create_environ(path="v1/doc")), response) + documentation = json.loads(response.data.decode("utf8"))["documentation"] + assert "versions" in documentation + assert "/echo" in documentation["handlers"] + assert "/test" not in documentation["handlers"] def test_basic_documentation_output_type_accept(): """Ensure API documentation works with selectable output types""" accept_output = hug.output_format.accept( - {'application/json': hug.output_format.json, - 'application/pretty-json': hug.output_format.pretty_json}, - default=hug.output_format.json) - with mock.patch.object(api.http, '_output_format', accept_output, create=True): + { + "application/json": hug.output_format.json, + "application/pretty-json": hug.output_format.pretty_json, + }, + default=hug.output_format.json, + ) + with mock.patch.object(api.http, "_output_format", accept_output, create=True): handler = api.http.documentation_404() response = StartResponseMock() - handler(Request(create_environ(path='v1/doc')), response) + handler(Request(create_environ(path="v1/doc")), response) - documentation = json.loads(response.data.decode('utf8'))['documentation'] - assert 'handlers' in documentation and 'overview' in documentation + documentation = json.loads(response.data.decode("utf8"))["documentation"] + assert "handlers" in documentation and "overview" in documentation - -def test_marshmallow_return_type_documentation(): +def test_marshmallow_return_type_documentation(): class Returns(marshmallow.Schema): "Return docs" @@ -178,4 +184,4 @@ def marshtest() -> Returns(): doc = api.http.documentation() - assert doc['handlers']['/marshtest']['POST']['outputs']['type'] == "Return docs" + assert doc["handlers"]["/marshtest"]["POST"]["outputs"]["type"] == "Return docs" diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index c8626fd8..07bc70eb 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -26,19 +26,19 @@ def test_invalid_type_data(): try: - raise hug.exceptions.InvalidTypeData('not a good type') + raise hug.exceptions.InvalidTypeData("not a good type") except hug.exceptions.InvalidTypeData as exception: error = exception - assert error.message == 'not a good type' + assert error.message == "not a good type" assert error.reasons is None try: - raise hug.exceptions.InvalidTypeData('not a good type', [1, 2, 3]) + raise hug.exceptions.InvalidTypeData("not a good type", [1, 2, 3]) except hug.exceptions.InvalidTypeData as exception: error = exception - assert error.message == 'not a good type' + assert error.message == "not a good type" assert error.reasons == [1, 2, 3] with pytest.raises(Exception): diff --git a/tests/test_global_context.py b/tests/test_global_context.py index bf976777..0bc5d301 100644 --- a/tests/test_global_context.py +++ b/tests/test_global_context.py @@ -2,30 +2,31 @@ def test_context_global_decorators(hug_api): - custom_context = dict(context='global', factory=0, delete=0) + custom_context = dict(context="global", factory=0, delete=0) @hug.context_factory(apply_globally=True) def create_context(*args, **kwargs): - custom_context['factory'] += 1 + custom_context["factory"] += 1 return custom_context @hug.delete_context(apply_globally=True) def delete_context(context, *args, **kwargs): assert context == custom_context - custom_context['delete'] += 1 + custom_context["delete"] += 1 @hug.get(api=hug_api) def made_up_hello(): - return 'hi' + return "hi" - @hug.extend_api(api=hug_api, base_url='/api') + @hug.extend_api(api=hug_api, base_url="/api") def extend_with(): import tests.module_fake_simple - return (tests.module_fake_simple, ) - - assert hug.test.get(hug_api, '/made_up_hello').data == 'hi' - assert custom_context['factory'] == 1 - assert custom_context['delete'] == 1 - assert hug.test.get(hug_api, '/api/made_up_hello').data == 'hello' - assert custom_context['factory'] == 2 - assert custom_context['delete'] == 2 + + return (tests.module_fake_simple,) + + assert hug.test.get(hug_api, "/made_up_hello").data == "hi" + assert custom_context["factory"] == 1 + assert custom_context["delete"] == 1 + assert hug.test.get(hug_api, "/api/made_up_hello").data == "hello" + assert custom_context["factory"] == 2 + assert custom_context["delete"] == 2 diff --git a/tests/test_input_format.py b/tests/test_input_format.py index afd5cdd1..da836ccb 100644 --- a/tests/test_input_format.py +++ b/tests/test_input_format.py @@ -39,27 +39,33 @@ def test_text(): def test_json(): """Ensure that the json input format works as intended""" test_data = BytesIO(b'{"a": "b"}') - assert hug.input_format.json(test_data) == {'a': 'b'} + assert hug.input_format.json(test_data) == {"a": "b"} def test_json_underscore(): """Ensure that camelCase keys can be converted into under_score for easier use within Python""" test_data = BytesIO(b'{"CamelCase": {"becauseWeCan": "ValueExempt"}}') - assert hug.input_format.json_underscore(test_data) == {'camel_case': {'because_we_can': 'ValueExempt'}} + assert hug.input_format.json_underscore(test_data) == { + "camel_case": {"because_we_can": "ValueExempt"} + } def test_urlencoded(): """Ensure that urlencoded input format works as intended""" - test_data = BytesIO(b'foo=baz&foo=bar&name=John+Doe') - assert hug.input_format.urlencoded(test_data) == {'name': 'John Doe', 'foo': ['baz', 'bar']} + test_data = BytesIO(b"foo=baz&foo=bar&name=John+Doe") + assert hug.input_format.urlencoded(test_data) == {"name": "John Doe", "foo": ["baz", "bar"]} def test_multipart(): """Ensure multipart form data works as intended""" - with open(os.path.join(BASE_DIRECTORY, 'artwork', 'koala.png'),'rb') as koala: - prepared_request = requests.Request('POST', 'http://localhost/', files={'koala': koala}).prepare() + with open(os.path.join(BASE_DIRECTORY, "artwork", "koala.png"), "rb") as koala: + prepared_request = requests.Request( + "POST", "http://localhost/", files={"koala": koala} + ).prepare() koala.seek(0) - headers = parse_header(prepared_request.headers['Content-Type'])[1] - headers['CONTENT-LENGTH'] = '22176' - file_content = hug.input_format.multipart(BytesIO(prepared_request.body), **headers)['koala'] + headers = parse_header(prepared_request.headers["Content-Type"])[1] + headers["CONTENT-LENGTH"] = "22176" + file_content = hug.input_format.multipart(BytesIO(prepared_request.body), **headers)[ + "koala" + ] assert file_content == koala.read() diff --git a/tests/test_interface.py b/tests/test_interface.py index ea678b76..2dae81ad 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -24,7 +24,7 @@ import hug -@hug.http(('/namer', '/namer/{name}'), ('GET', 'POST'), versions=(None, 2)) +@hug.http(("/namer", "/namer/{name}"), ("GET", "POST"), versions=(None, 2)) def namer(name=None): return name @@ -34,28 +34,33 @@ class TestHTTP(object): def test_urls(self): """Test to ensure HTTP interface correctly returns URLs associated with it""" - assert namer.interface.http.urls() == ['/namer', '/namer/{name}'] + assert namer.interface.http.urls() == ["/namer", "/namer/{name}"] def test_url(self): """Test to ensure HTTP interface correctly automatically returns URL associated with it""" - assert namer.interface.http.url() == '/namer' - assert namer.interface.http.url(name='tim') == '/namer/tim' - assert namer.interface.http.url(name='tim', version=2) == '/v2/namer/tim' + assert namer.interface.http.url() == "/namer" + assert namer.interface.http.url(name="tim") == "/namer/tim" + assert namer.interface.http.url(name="tim", version=2) == "/v2/namer/tim" with pytest.raises(KeyError): - namer.interface.http.url(undefined='not a variable') + namer.interface.http.url(undefined="not a variable") with pytest.raises(KeyError): namer.interface.http.url(version=10) def test_gather_parameters(self): """Test to ensure gathering parameters works in the expected way""" + @hug.get() def my_example_api(body): return body - assert hug.test.get(__hug__, 'my_example_api', body='', - headers={'content-type': 'application/json'}).data == None + assert ( + hug.test.get( + __hug__, "my_example_api", body="", headers={"content-type": "application/json"} + ).data + == None + ) class TestLocal(object): diff --git a/tests/test_introspect.py b/tests/test_introspect.py index e997f744..064d1c2e 100644 --- a/tests/test_introspect.py +++ b/tests/test_introspect.py @@ -43,7 +43,6 @@ def function_with_nothing(): class Object(object): - def my_method(self): pass @@ -56,13 +55,18 @@ def test_is_method(): def test_arguments(): """Test to ensure hug introspection can correctly pull out arguments from a function definition""" + def function(argument1, argument2): pass - assert tuple(hug.introspect.arguments(function_with_kwargs)) == ('argument1', ) - assert tuple(hug.introspect.arguments(function_with_args)) == ('argument1', ) - assert tuple(hug.introspect.arguments(function_with_neither)) == ('argument1', 'argument2') - assert tuple(hug.introspect.arguments(function_with_both)) == ('argument1', 'argument2', 'argument3') + assert tuple(hug.introspect.arguments(function_with_kwargs)) == ("argument1",) + assert tuple(hug.introspect.arguments(function_with_args)) == ("argument1",) + assert tuple(hug.introspect.arguments(function_with_neither)) == ("argument1", "argument2") + assert tuple(hug.introspect.arguments(function_with_both)) == ( + "argument1", + "argument2", + "argument3", + ) def test_takes_kwargs(): @@ -83,35 +87,56 @@ def test_takes_args(): def test_takes_arguments(): """Test to ensure hug introspection can correctly identify which arguments supplied a function will take""" - assert hug.introspect.takes_arguments(function_with_kwargs, 'argument1', 'argument3') == set(('argument1', )) - assert hug.introspect.takes_arguments(function_with_args, 'bacon') == set() - assert hug.introspect.takes_arguments(function_with_neither, - 'argument1', 'argument2') == set(('argument1', 'argument2')) - assert hug.introspect.takes_arguments(function_with_both, 'argument3', 'bacon') == set(('argument3', )) + assert hug.introspect.takes_arguments(function_with_kwargs, "argument1", "argument3") == set( + ("argument1",) + ) + assert hug.introspect.takes_arguments(function_with_args, "bacon") == set() + assert hug.introspect.takes_arguments(function_with_neither, "argument1", "argument2") == set( + ("argument1", "argument2") + ) + assert hug.introspect.takes_arguments(function_with_both, "argument3", "bacon") == set( + ("argument3",) + ) def test_takes_all_arguments(): """Test to ensure hug introspection can correctly identify if a function takes all specified arguments""" - assert not hug.introspect.takes_all_arguments(function_with_kwargs, 'argument1', 'argument2', 'argument3') - assert not hug.introspect.takes_all_arguments(function_with_args, 'argument1', 'argument2', 'argument3') - assert not hug.introspect.takes_all_arguments(function_with_neither, 'argument1', 'argument2', 'argument3') - assert hug.introspect.takes_all_arguments(function_with_both, 'argument1', 'argument2', 'argument3') + assert not hug.introspect.takes_all_arguments( + function_with_kwargs, "argument1", "argument2", "argument3" + ) + assert not hug.introspect.takes_all_arguments( + function_with_args, "argument1", "argument2", "argument3" + ) + assert not hug.introspect.takes_all_arguments( + function_with_neither, "argument1", "argument2", "argument3" + ) + assert hug.introspect.takes_all_arguments( + function_with_both, "argument1", "argument2", "argument3" + ) def test_generate_accepted_kwargs(): """Test to ensure hug introspection can correctly dynamically filter out kwargs for only those accepted""" - source_dictionary = {'argument1': 1, 'argument2': 2, 'hey': 'there', 'hi': 'hello'} + source_dictionary = {"argument1": 1, "argument2": 2, "hey": "there", "hi": "hello"} - kwargs = hug.introspect.generate_accepted_kwargs(function_with_kwargs, 'bacon', 'argument1')(source_dictionary) + kwargs = hug.introspect.generate_accepted_kwargs(function_with_kwargs, "bacon", "argument1")( + source_dictionary + ) assert kwargs == source_dictionary - kwargs = hug.introspect.generate_accepted_kwargs(function_with_args, 'bacon', 'argument1')(source_dictionary) - assert kwargs == {'argument1': 1} + kwargs = hug.introspect.generate_accepted_kwargs(function_with_args, "bacon", "argument1")( + source_dictionary + ) + assert kwargs == {"argument1": 1} - kwargs = hug.introspect.generate_accepted_kwargs(function_with_neither, 'argument1', 'argument2')(source_dictionary) - assert kwargs == {'argument1': 1, 'argument2': 2} + kwargs = hug.introspect.generate_accepted_kwargs( + function_with_neither, "argument1", "argument2" + )(source_dictionary) + assert kwargs == {"argument1": 1, "argument2": 2} - kwargs = hug.introspect.generate_accepted_kwargs(function_with_both, 'argument1', 'argument2')(source_dictionary) + kwargs = hug.introspect.generate_accepted_kwargs(function_with_both, "argument1", "argument2")( + source_dictionary + ) assert kwargs == source_dictionary kwargs = hug.introspect.generate_accepted_kwargs(function_with_nothing)(source_dictionary) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index aad7c69a..219e49fd 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -35,42 +35,42 @@ def test_session_middleware(): @hug.get() def count(request): - session = request.context['session'] - counter = session.get('counter', 0) + 1 - session['counter'] = counter + session = request.context["session"] + counter = session.get("counter", 0) + 1 + session["counter"] = counter return counter def get_cookies(response): - simple_cookie = SimpleCookie(response.headers_dict['set-cookie']) + simple_cookie = SimpleCookie(response.headers_dict["set-cookie"]) return {morsel.key: morsel.value for morsel in simple_cookie.values()} # Add middleware session_store = InMemoryStore() - middleware = SessionMiddleware(session_store, cookie_name='test-sid') + middleware = SessionMiddleware(session_store, cookie_name="test-sid") __hug__.http.add_middleware(middleware) # Get cookies from response - response = hug.test.get(api, '/count') + response = hug.test.get(api, "/count") cookies = get_cookies(response) # Assert session cookie has been set and session exists in session store - assert 'test-sid' in cookies - sid = cookies['test-sid'] + assert "test-sid" in cookies + sid = cookies["test-sid"] assert session_store.exists(sid) - assert session_store.get(sid) == {'counter': 1} + assert session_store.get(sid) == {"counter": 1} # Assert session persists throughout the requests - headers = {'Cookie': 'test-sid={}'.format(sid)} - assert hug.test.get(api, '/count', headers=headers).data == 2 - assert session_store.get(sid) == {'counter': 2} + headers = {"Cookie": "test-sid={}".format(sid)} + assert hug.test.get(api, "/count", headers=headers).data == 2 + assert session_store.get(sid) == {"counter": 2} # Assert a non-existing session cookie gets ignored - headers = {'Cookie': 'test-sid=foobarfoo'} - response = hug.test.get(api, '/count', headers=headers) + headers = {"Cookie": "test-sid=foobarfoo"} + response = hug.test.get(api, "/count", headers=headers) cookies = get_cookies(response) assert response.data == 1 - assert not session_store.exists('foobarfoo') - assert cookies['test-sid'] != 'foobarfoo' + assert not session_store.exists("foobarfoo") + assert cookies["test-sid"] != "foobarfoo" def test_logging_middleware(): @@ -87,67 +87,69 @@ def __init__(self, logger=Logger()): @hug.get() def test(request): - return 'data' + return "data" - hug.test.get(api, '/test') - assert output[0] == 'Requested: GET /test None' + hug.test.get(api, "/test") + assert output[0] == "Requested: GET /test None" assert len(output[1]) > 0 def test_cors_middleware(hug_api): hug_api.http.add_middleware(CORSMiddleware(hug_api, max_age=10)) - @hug.get('/demo', api=hug_api) + @hug.get("/demo", api=hug_api) def get_demo(): - return {'result': 'Hello World'} + return {"result": "Hello World"} - @hug.get('/demo/{param}', api=hug_api) + @hug.get("/demo/{param}", api=hug_api) def get_demo(param): - return {'result': 'Hello {0}'.format(param)} + return {"result": "Hello {0}".format(param)} - @hug.post('/demo', api=hug_api) - def post_demo(name: 'your name'): - return {'result': 'Hello {0}'.format(name)} + @hug.post("/demo", api=hug_api) + def post_demo(name: "your name"): + return {"result": "Hello {0}".format(name)} - @hug.put('/demo/{param}', api=hug_api) + @hug.put("/demo/{param}", api=hug_api) def get_demo(param, name): old_name = param new_name = name - return {'result': 'Goodbye {0} ... Hello {1}'.format(old_name, new_name)} + return {"result": "Goodbye {0} ... Hello {1}".format(old_name, new_name)} - @hug.delete('/demo/{param}', api=hug_api) + @hug.delete("/demo/{param}", api=hug_api) def get_demo(param): - return {'result': 'Goodbye {0}'.format(param)} - - assert hug.test.get(hug_api, '/demo').data == {'result': 'Hello World'} - assert hug.test.get(hug_api, '/demo/Mir').data == {'result': 'Hello Mir'} - assert hug.test.post(hug_api, '/demo', name='Mundo') - assert hug.test.put(hug_api, '/demo/Carl', name='Junior').data == {'result': 'Goodbye Carl ... Hello Junior'} - assert hug.test.delete(hug_api, '/demo/Cruel_World').data == {'result': 'Goodbye Cruel_World'} - - response = hug.test.options(hug_api, '/demo') - methods = response.headers_dict['access-control-allow-methods'].replace(' ', '') - allow = response.headers_dict['allow'].replace(' ', '') - assert set(methods.split(',')) == set(['OPTIONS', 'GET', 'POST']) - assert set(allow.split(',')) == set(['OPTIONS', 'GET', 'POST']) - - response = hug.test.options(hug_api, '/demo/1') - methods = response.headers_dict['access-control-allow-methods'].replace(' ', '') - allow = response.headers_dict['allow'].replace(' ', '') - assert set(methods.split(',')) == set(['OPTIONS', 'GET', 'DELETE', 'PUT']) - assert set(allow.split(',')) == set(['OPTIONS', 'GET', 'DELETE', 'PUT']) - assert response.headers_dict['access-control-max-age'] == '10' - - response = hug.test.options(hug_api, '/v1/demo/1') - methods = response.headers_dict['access-control-allow-methods'].replace(' ', '') - allow = response.headers_dict['allow'].replace(' ', '') - assert set(methods.split(',')) == set(['OPTIONS', 'GET', 'DELETE', 'PUT']) - assert set(allow.split(',')) == set(['OPTIONS', 'GET', 'DELETE', 'PUT']) - assert response.headers_dict['access-control-max-age'] == '10' - - response = hug.test.options(hug_api, '/v1/demo/123e4567-midlee89b-12d3-a456-426655440000') - methods = response.headers_dict['access-control-allow-methods'].replace(' ', '') - allow = response.headers_dict['allow'].replace(' ', '') - assert set(methods.split(',')) == set(['OPTIONS', 'GET', 'DELETE', 'PUT']) - assert set(allow.split(',')) == set(['OPTIONS', 'GET', 'DELETE', 'PUT']) - assert response.headers_dict['access-control-max-age'] == '10' + return {"result": "Goodbye {0}".format(param)} + + assert hug.test.get(hug_api, "/demo").data == {"result": "Hello World"} + assert hug.test.get(hug_api, "/demo/Mir").data == {"result": "Hello Mir"} + assert hug.test.post(hug_api, "/demo", name="Mundo") + assert hug.test.put(hug_api, "/demo/Carl", name="Junior").data == { + "result": "Goodbye Carl ... Hello Junior" + } + assert hug.test.delete(hug_api, "/demo/Cruel_World").data == {"result": "Goodbye Cruel_World"} + + response = hug.test.options(hug_api, "/demo") + methods = response.headers_dict["access-control-allow-methods"].replace(" ", "") + allow = response.headers_dict["allow"].replace(" ", "") + assert set(methods.split(",")) == set(["OPTIONS", "GET", "POST"]) + assert set(allow.split(",")) == set(["OPTIONS", "GET", "POST"]) + + response = hug.test.options(hug_api, "/demo/1") + methods = response.headers_dict["access-control-allow-methods"].replace(" ", "") + allow = response.headers_dict["allow"].replace(" ", "") + assert set(methods.split(",")) == set(["OPTIONS", "GET", "DELETE", "PUT"]) + assert set(allow.split(",")) == set(["OPTIONS", "GET", "DELETE", "PUT"]) + assert response.headers_dict["access-control-max-age"] == "10" + + response = hug.test.options(hug_api, "/v1/demo/1") + methods = response.headers_dict["access-control-allow-methods"].replace(" ", "") + allow = response.headers_dict["allow"].replace(" ", "") + assert set(methods.split(",")) == set(["OPTIONS", "GET", "DELETE", "PUT"]) + assert set(allow.split(",")) == set(["OPTIONS", "GET", "DELETE", "PUT"]) + assert response.headers_dict["access-control-max-age"] == "10" + + response = hug.test.options(hug_api, "/v1/demo/123e4567-midlee89b-12d3-a456-426655440000") + methods = response.headers_dict["access-control-allow-methods"].replace(" ", "") + allow = response.headers_dict["allow"].replace(" ", "") + assert set(methods.split(",")) == set(["OPTIONS", "GET", "DELETE", "PUT"]) + assert set(allow.split(",")) == set(["OPTIONS", "GET", "DELETE", "PUT"]) + assert response.headers_dict["access-control-max-age"] == "10" diff --git a/tests/test_output_format.py b/tests/test_output_format.py index d9fe3455..b28e05a3 100644 --- a/tests/test_output_format.py +++ b/tests/test_output_format.py @@ -44,48 +44,50 @@ def test_html(hug_api): """Ensure that it's possible to output a Hug API method as HTML""" hug.output_format.html("Hello World!") == "Hello World!" hug.output_format.html(str(1)) == "1" - with open(os.path.join(BASE_DIRECTORY, 'README.md'), 'rb') as html_file: - assert hasattr(hug.output_format.html(html_file), 'read') + with open(os.path.join(BASE_DIRECTORY, "README.md"), "rb") as html_file: + assert hasattr(hug.output_format.html(html_file), "read") - class FakeHTMLWithRender(): + class FakeHTMLWithRender: def render(self): - return 'test' + return "test" - assert hug.output_format.html(FakeHTMLWithRender()) == b'test' + assert hug.output_format.html(FakeHTMLWithRender()) == b"test" - @hug.get('/get/html', output=hug.output_format.html, api=hug_api) + @hug.get("/get/html", output=hug.output_format.html, api=hug_api) def get_html(**kwargs): """ Returns command help document when no command is specified """ - with open(os.path.join(BASE_DIRECTORY, 'examples/document.html'), 'rb') as html_file: + with open(os.path.join(BASE_DIRECTORY, "examples/document.html"), "rb") as html_file: return html_file.read() - assert '' in hug.test.get(hug_api, '/get/html').data + assert "" in hug.test.get(hug_api, "/get/html").data def test_json(): """Ensure that it's possible to output a Hug API method as JSON""" now = datetime.now() one_day = timedelta(days=1) - test_data = {'text': 'text', 'datetime': now, 'bytes': b'bytes', 'delta': one_day} - output = hug.output_format.json(test_data).decode('utf8') - assert 'text' in output - assert 'bytes' in output + test_data = {"text": "text", "datetime": now, "bytes": b"bytes", "delta": one_day} + output = hug.output_format.json(test_data).decode("utf8") + assert "text" in output + assert "bytes" in output assert str(one_day.total_seconds()) in output assert now.isoformat() in output class NewObject(object): pass - test_data['non_serializable'] = NewObject() + + test_data["non_serializable"] = NewObject() with pytest.raises(TypeError): - hug.output_format.json(test_data).decode('utf8') + hug.output_format.json(test_data).decode("utf8") - class NamedTupleObject(namedtuple('BaseTuple', ('name', 'value'))): + class NamedTupleObject(namedtuple("BaseTuple", ("name", "value"))): pass - data = NamedTupleObject('name', 'value') + + data = NamedTupleObject("name", "value") converted = hug.input_format.json(BytesIO(hug.output_format.json(data))) - assert converted == {'name': 'name', 'value': 'value'} + assert converted == {"name": "name", "value": "value"} data = set((1, 2, 3, 3)) assert hug.input_format.json(BytesIO(hug.output_format.json(data))) == [1, 2, 3] @@ -94,238 +96,269 @@ class NamedTupleObject(namedtuple('BaseTuple', ('name', 'value'))): assert hug.input_format.json(BytesIO(hug.output_format.json(data))) == [1, 2, 3] data = [Decimal(1.5), Decimal("155.23"), Decimal("1234.25")] - assert hug.input_format.json(BytesIO(hug.output_format.json(data))) == ["1.5", "155.23", "1234.25"] + assert hug.input_format.json(BytesIO(hug.output_format.json(data))) == [ + "1.5", + "155.23", + "1234.25", + ] - with open(os.path.join(BASE_DIRECTORY, 'README.md'), 'rb') as json_file: - assert hasattr(hug.output_format.json(json_file), 'read') + with open(os.path.join(BASE_DIRECTORY, "README.md"), "rb") as json_file: + assert hasattr(hug.output_format.json(json_file), "read") - assert hug.input_format.json(BytesIO(hug.output_format.json(b'\x9c'))) == 'nA==' + assert hug.input_format.json(BytesIO(hug.output_format.json(b"\x9c"))) == "nA==" class MyCrazyObject(object): pass @hug.output_format.json_convert(MyCrazyObject) def convert(instance): - return 'Like anyone could convert this' + return "Like anyone could convert this" - assert hug.input_format.json(BytesIO(hug.output_format.json(MyCrazyObject()))) == 'Like anyone could convert this' - assert hug.input_format.json(BytesIO(hug.output_format.json({'data': ['Τη γλώσσα μου έδωσαν ελληνική']}))) == \ - {'data': ['Τη γλώσσα μου έδωσαν ελληνική']} + assert ( + hug.input_format.json(BytesIO(hug.output_format.json(MyCrazyObject()))) + == "Like anyone could convert this" + ) + assert hug.input_format.json( + BytesIO(hug.output_format.json({"data": ["Τη γλώσσα μου έδωσαν ελληνική"]})) + ) == {"data": ["Τη γλώσσα μου έδωσαν ελληνική"]} def test_pretty_json(): """Ensure that it's possible to output a Hug API method as prettified and indented JSON""" - test_data = {'text': 'text'} - assert hug.output_format.pretty_json(test_data).decode('utf8') == ('{\n' - ' "text": "text"\n' - '}') + test_data = {"text": "text"} + assert hug.output_format.pretty_json(test_data).decode("utf8") == ( + "{\n" ' "text": "text"\n' "}" + ) def test_json_camelcase(): """Ensure that it's possible to output a Hug API method as camelCased JSON""" - test_data = {'under_score': 'values_can', 'be_converted': [{'to_camelcase': 'value'}, 'wont_be_convert']} - output = hug.output_format.json_camelcase(test_data).decode('utf8') - assert 'underScore' in output - assert 'values_can' in output - assert 'beConverted' in output - assert 'toCamelcase' in output - assert 'value' in output - assert 'wont_be_convert' in output + test_data = { + "under_score": "values_can", + "be_converted": [{"to_camelcase": "value"}, "wont_be_convert"], + } + output = hug.output_format.json_camelcase(test_data).decode("utf8") + assert "underScore" in output + assert "values_can" in output + assert "beConverted" in output + assert "toCamelcase" in output + assert "value" in output + assert "wont_be_convert" in output def test_image(): """Ensure that it's possible to output images with hug""" - logo_path = os.path.join(BASE_DIRECTORY, 'artwork', 'logo.png') - assert hasattr(hug.output_format.png_image(logo_path, hug.Response()), 'read') - with open(logo_path, 'rb') as image_file: - assert hasattr(hug.output_format.png_image(image_file, hug.Response()), 'read') + logo_path = os.path.join(BASE_DIRECTORY, "artwork", "logo.png") + assert hasattr(hug.output_format.png_image(logo_path, hug.Response()), "read") + with open(logo_path, "rb") as image_file: + assert hasattr(hug.output_format.png_image(image_file, hug.Response()), "read") - assert hug.output_format.png_image('Not Existent', hug.Response()) is None + assert hug.output_format.png_image("Not Existent", hug.Response()) is None - class FakeImageWithSave(): + class FakeImageWithSave: def save(self, to, format): - to.write(b'test') - assert hasattr(hug.output_format.png_image(FakeImageWithSave(), hug.Response()), 'read') + to.write(b"test") + + assert hasattr(hug.output_format.png_image(FakeImageWithSave(), hug.Response()), "read") - class FakeImageWithRender(): + class FakeImageWithRender: def render(self): - return 'test' - assert hug.output_format.svg_xml_image(FakeImageWithRender(), hug.Response()) == 'test' + return "test" - class FakeImageWithSaveNoFormat(): + assert hug.output_format.svg_xml_image(FakeImageWithRender(), hug.Response()) == "test" + + class FakeImageWithSaveNoFormat: def save(self, to): - to.write(b'test') - assert hasattr(hug.output_format.png_image(FakeImageWithSaveNoFormat(), hug.Response()), 'read') + to.write(b"test") + + assert hasattr(hug.output_format.png_image(FakeImageWithSaveNoFormat(), hug.Response()), "read") def test_file(): """Ensure that it's possible to easily output files""" + class FakeResponse(object): pass - logo_path = os.path.join(BASE_DIRECTORY, 'artwork', 'logo.png') + logo_path = os.path.join(BASE_DIRECTORY, "artwork", "logo.png") fake_response = FakeResponse() - assert hasattr(hug.output_format.file(logo_path, fake_response), 'read') - assert fake_response.content_type == 'image/png' - with open(logo_path, 'rb') as image_file: - hasattr(hug.output_format.file(image_file, fake_response), 'read') + assert hasattr(hug.output_format.file(logo_path, fake_response), "read") + assert fake_response.content_type == "image/png" + with open(logo_path, "rb") as image_file: + hasattr(hug.output_format.file(image_file, fake_response), "read") - assert not hasattr(hug.output_format.file('NON EXISTENT FILE', fake_response), 'read') - assert hug.output_format.file(None, fake_response) == '' + assert not hasattr(hug.output_format.file("NON EXISTENT FILE", fake_response), "read") + assert hug.output_format.file(None, fake_response) == "" def test_video(): """Ensure that it's possible to output videos with hug""" - gif_path = os.path.join(BASE_DIRECTORY, 'artwork', 'example.gif') - assert hasattr(hug.output_format.mp4_video(gif_path, hug.Response()), 'read') - with open(gif_path, 'rb') as image_file: - assert hasattr(hug.output_format.mp4_video(image_file, hug.Response()), 'read') + gif_path = os.path.join(BASE_DIRECTORY, "artwork", "example.gif") + assert hasattr(hug.output_format.mp4_video(gif_path, hug.Response()), "read") + with open(gif_path, "rb") as image_file: + assert hasattr(hug.output_format.mp4_video(image_file, hug.Response()), "read") - assert hug.output_format.mp4_video('Not Existent', hug.Response()) is None + assert hug.output_format.mp4_video("Not Existent", hug.Response()) is None - class FakeVideoWithSave(): + class FakeVideoWithSave: def save(self, to, format): - to.write(b'test') - assert hasattr(hug.output_format.mp4_video(FakeVideoWithSave(), hug.Response()), 'read') + to.write(b"test") - class FakeVideoWithSave(): + assert hasattr(hug.output_format.mp4_video(FakeVideoWithSave(), hug.Response()), "read") + + class FakeVideoWithSave: def render(self): - return 'test' - assert hug.output_format.avi_video(FakeVideoWithSave(), hug.Response()) == 'test' + return "test" + + assert hug.output_format.avi_video(FakeVideoWithSave(), hug.Response()) == "test" def test_on_valid(): """Test to ensure formats that use on_valid content types gracefully handle error dictionaries""" - error_dict = {'errors': {'so': 'many'}} + error_dict = {"errors": {"so": "many"}} expected = hug.output_format.json(error_dict) assert hug.output_format.mp4_video(error_dict, hug.Response()) == expected assert hug.output_format.png_image(error_dict, hug.Response()) == expected - @hug.output_format.on_valid('image', hug.output_format.file) + @hug.output_format.on_valid("image", hug.output_format.file) def my_output_format(data): - raise ValueError('This should never be called') + raise ValueError("This should never be called") assert my_output_format(error_dict, hug.Response()) def test_on_content_type(): """Ensure that it's possible to route the output type format by the requested content-type""" - formatter = hug.output_format.on_content_type({'application/json': hug.output_format.json, - 'text/plain': hug.output_format.text}) + formatter = hug.output_format.on_content_type( + {"application/json": hug.output_format.json, "text/plain": hug.output_format.text} + ) class FakeRequest(object): - content_type = 'application/json' + content_type = "application/json" request = FakeRequest() response = FakeRequest() - converted = hug.input_format.json(formatter(BytesIO(hug.output_format.json({'name': 'name'})), request, response)) - assert converted == {'name': 'name'} + converted = hug.input_format.json( + formatter(BytesIO(hug.output_format.json({"name": "name"})), request, response) + ) + assert converted == {"name": "name"} - request.content_type = 'text/plain' - assert formatter('hi', request, response) == b'hi' + request.content_type = "text/plain" + assert formatter("hi", request, response) == b"hi" with pytest.raises(hug.HTTPNotAcceptable): - request.content_type = 'undefined; always' - formatter('hi', request, response) + request.content_type = "undefined; always" + formatter("hi", request, response) def test_accept(): """Ensure that it's possible to route the output type format by the requests stated accept header""" - formatter = hug.output_format.accept({'application/json': hug.output_format.json, - 'text/plain': hug.output_format.text}) + formatter = hug.output_format.accept( + {"application/json": hug.output_format.json, "text/plain": hug.output_format.text} + ) class FakeRequest(object): - accept = 'application/json' + accept = "application/json" request = FakeRequest() response = FakeRequest() - converted = hug.input_format.json(formatter(BytesIO(hug.output_format.json({'name': 'name'})), request, response)) - assert converted == {'name': 'name'} + converted = hug.input_format.json( + formatter(BytesIO(hug.output_format.json({"name": "name"})), request, response) + ) + assert converted == {"name": "name"} - request.accept = 'text/plain' - assert formatter('hi', request, response) == b'hi' + request.accept = "text/plain" + assert formatter("hi", request, response) == b"hi" - request.accept = 'application/json, text/plain; q=0.5' - assert formatter('hi', request, response) == b'"hi"' + request.accept = "application/json, text/plain; q=0.5" + assert formatter("hi", request, response) == b'"hi"' - request.accept = 'text/plain; q=0.5, application/json' - assert formatter('hi', request, response) == b'"hi"' + request.accept = "text/plain; q=0.5, application/json" + assert formatter("hi", request, response) == b'"hi"' - request.accept = 'application/json;q=0.4,text/plain; q=0.5' - assert formatter('hi', request, response) == b'hi' + request.accept = "application/json;q=0.4,text/plain; q=0.5" + assert formatter("hi", request, response) == b"hi" - request.accept = '*' - assert formatter('hi', request, response) in [b'"hi"', b'hi'] + request.accept = "*" + assert formatter("hi", request, response) in [b'"hi"', b"hi"] - request.accept = 'undefined; always' + request.accept = "undefined; always" with pytest.raises(hug.HTTPNotAcceptable): - formatter('hi', request, response) - + formatter("hi", request, response) - formatter = hug.output_format.accept({'application/json': hug.output_format.json, - 'text/plain': hug.output_format.text}, hug.output_format.json) - assert formatter('hi', request, response) == b'"hi"' + formatter = hug.output_format.accept( + {"application/json": hug.output_format.json, "text/plain": hug.output_format.text}, + hug.output_format.json, + ) + assert formatter("hi", request, response) == b'"hi"' def test_accept_with_http_errors(): """Ensure that content type based output formats work for HTTP error responses""" - formatter = hug.output_format.accept({'application/json': hug.output_format.json, - 'text/plain': hug.output_format.text}, - default=hug.output_format.json) + formatter = hug.output_format.accept( + {"application/json": hug.output_format.json, "text/plain": hug.output_format.text}, + default=hug.output_format.json, + ) - api = hug.API('test_accept_with_http_errors') + api = hug.API("test_accept_with_http_errors") hug.default_output_format(api=api)(formatter) - @hug.get('/500', api=api) + @hug.get("/500", api=api) def error_500(): - raise hug.HTTPInternalServerError('500 Internal Server Error', - 'This is an example') + raise hug.HTTPInternalServerError("500 Internal Server Error", "This is an example") - response = hug.test.get(api, '/500') - assert response.status == '500 Internal Server Error' - assert response.data == { - 'errors': {'500 Internal Server Error': 'This is an example'}} + response = hug.test.get(api, "/500") + assert response.status == "500 Internal Server Error" + assert response.data == {"errors": {"500 Internal Server Error": "This is an example"}} def test_suffix(): """Ensure that it's possible to route the output type format by the suffix of the requested URL""" - formatter = hug.output_format.suffix({'.js': hug.output_format.json, '.html': hug.output_format.text}) + formatter = hug.output_format.suffix( + {".js": hug.output_format.json, ".html": hug.output_format.text} + ) class FakeRequest(object): - path = 'endpoint.js' + path = "endpoint.js" request = FakeRequest() response = FakeRequest() - converted = hug.input_format.json(formatter(BytesIO(hug.output_format.json({'name': 'name'})), request, response)) - assert converted == {'name': 'name'} + converted = hug.input_format.json( + formatter(BytesIO(hug.output_format.json({"name": "name"})), request, response) + ) + assert converted == {"name": "name"} - request.path = 'endpoint.html' - assert formatter('hi', request, response) == b'hi' + request.path = "endpoint.html" + assert formatter("hi", request, response) == b"hi" with pytest.raises(hug.HTTPNotAcceptable): - request.path = 'undefined.always' - formatter('hi', request, response) + request.path = "undefined.always" + formatter("hi", request, response) def test_prefix(): """Ensure that it's possible to route the output type format by the prefix of the requested URL""" - formatter = hug.output_format.prefix({'js/': hug.output_format.json, 'html/': hug.output_format.text}) + formatter = hug.output_format.prefix( + {"js/": hug.output_format.json, "html/": hug.output_format.text} + ) class FakeRequest(object): - path = 'js/endpoint' + path = "js/endpoint" request = FakeRequest() response = FakeRequest() - converted = hug.input_format.json(formatter(BytesIO(hug.output_format.json({'name': 'name'})), request, response)) - assert converted == {'name': 'name'} + converted = hug.input_format.json( + formatter(BytesIO(hug.output_format.json({"name": "name"})), request, response) + ) + assert converted == {"name": "name"} - request.path = 'html/endpoint' - assert formatter('hi', request, response) == b'hi' + request.path = "html/endpoint" + assert formatter("hi", request, response) == b"hi" with pytest.raises(hug.HTTPNotAcceptable): - request.path = 'undefined.always' - formatter('hi', request, response) + request.path = "undefined.always" + formatter("hi", request, response) def test_json_converter_numpy_types(): @@ -333,21 +366,45 @@ def test_json_converter_numpy_types(): ex_int = numpy.int_(9) ex_np_array = numpy.array([1, 2, 3, 4, 5]) ex_np_int_array = numpy.int_([5, 4, 3]) - ex_np_float = numpy.float(.5) + ex_np_float = numpy.float(0.5) assert 9 is hug.output_format._json_converter(ex_int) assert [1, 2, 3, 4, 5] == hug.output_format._json_converter(ex_np_array) assert [5, 4, 3] == hug.output_format._json_converter(ex_np_int_array) - assert .5 == hug.output_format._json_converter(ex_np_float) + assert 0.5 == hug.output_format._json_converter(ex_np_float) # Some type names are merely shorthands. # The following shorthands for built-in types are excluded: numpy.bool, numpy.int, numpy.float. np_bool_types = [numpy.bool_, numpy.bool8] - np_int_types = [numpy.int_, numpy.byte, numpy.ubyte, numpy.intc, numpy.uintc, numpy.intp, numpy.uintp, numpy.int8, - numpy.uint8, numpy.int16, numpy.uint16, numpy.int32, numpy.uint32, numpy.int64, numpy.uint64, - numpy.longlong, numpy.ulonglong, numpy.short, numpy.ushort] - np_float_types = [numpy.float_, numpy.float32, numpy.float64, numpy.half, numpy.single, - numpy.longfloat] + np_int_types = [ + numpy.int_, + numpy.byte, + numpy.ubyte, + numpy.intc, + numpy.uintc, + numpy.intp, + numpy.uintp, + numpy.int8, + numpy.uint8, + numpy.int16, + numpy.uint16, + numpy.int32, + numpy.uint32, + numpy.int64, + numpy.uint64, + numpy.longlong, + numpy.ulonglong, + numpy.short, + numpy.ushort, + ] + np_float_types = [ + numpy.float_, + numpy.float32, + numpy.float64, + numpy.half, + numpy.single, + numpy.longfloat, + ] np_unicode_types = [numpy.unicode_] np_bytes_types = [numpy.bytes_] @@ -356,23 +413,24 @@ def test_json_converter_numpy_types(): for np_type in np_int_types: assert 1 is hug.output_format._json_converter(np_type(1)) for np_type in np_float_types: - assert .5 == hug.output_format._json_converter(np_type(.5)) + assert 0.5 == hug.output_format._json_converter(np_type(0.5)) for np_type in np_unicode_types: - assert "a" == hug.output_format._json_converter(np_type('a')) + assert "a" == hug.output_format._json_converter(np_type("a")) for np_type in np_bytes_types: - assert "a" == hug.output_format._json_converter(np_type('a')) + assert "a" == hug.output_format._json_converter(np_type("a")) + def test_json_converter_uuid(): """Ensure that uuid data type is properly supported in JSON output.""" - uuidstr = '8ae4d8c1-e2d7-5cd0-8407-6baf16dfbca4' + uuidstr = "8ae4d8c1-e2d7-5cd0-8407-6baf16dfbca4" assert uuidstr == hug.output_format._json_converter(UUID(uuidstr)) - + def test_output_format_with_no_docstring(): """Ensure it is safe to use formatters with no docstring""" - @hug.format.content_type('test/fmt') + @hug.format.content_type("test/fmt") def test_fmt(data, request=None, response=None): - return str(data).encode('utf8') + return str(data).encode("utf8") - hug.output_format.on_content_type({'test/fmt': test_fmt}) + hug.output_format.on_content_type({"test/fmt": test_fmt}) diff --git a/tests/test_redirect.py b/tests/test_redirect.py index 9f181068..4936fd61 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -28,39 +28,39 @@ def test_to(): """Test that the base redirect to function works as expected""" with pytest.raises(falcon.http_status.HTTPStatus) as redirect: - hug.redirect.to('/') - assert '302' in redirect.value.status + hug.redirect.to("/") + assert "302" in redirect.value.status def test_permanent(): """Test to ensure function causes a redirect with HTTP 301 status code""" with pytest.raises(falcon.http_status.HTTPStatus) as redirect: - hug.redirect.permanent('/') - assert '301' in redirect.value.status + hug.redirect.permanent("/") + assert "301" in redirect.value.status def test_found(): """Test to ensure function causes a redirect with HTTP 302 status code""" with pytest.raises(falcon.http_status.HTTPStatus) as redirect: - hug.redirect.found('/') - assert '302' in redirect.value.status + hug.redirect.found("/") + assert "302" in redirect.value.status def test_see_other(): """Test to ensure function causes a redirect with HTTP 303 status code""" with pytest.raises(falcon.http_status.HTTPStatus) as redirect: - hug.redirect.see_other('/') - assert '303' in redirect.value.status + hug.redirect.see_other("/") + assert "303" in redirect.value.status def test_temporary(): """Test to ensure function causes a redirect with HTTP 307 status code""" with pytest.raises(falcon.http_status.HTTPStatus) as redirect: - hug.redirect.temporary('/') - assert '307' in redirect.value.status + hug.redirect.temporary("/") + assert "307" in redirect.value.status def test_not_found(): with pytest.raises(falcon.HTTPNotFound) as redirect: hug.redirect.not_found() - assert '404' in redirect.value.status + assert "404" in redirect.value.status diff --git a/tests/test_route.py b/tests/test_route.py index 06de873d..3ef8230c 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -20,153 +20,162 @@ """ import hug -from hug.routing import CLIRouter, ExceptionRouter, NotFoundRouter, SinkRouter, StaticRouter, URLRouter +from hug.routing import ( + CLIRouter, + ExceptionRouter, + NotFoundRouter, + SinkRouter, + StaticRouter, + URLRouter, +) api = hug.API(__name__) def test_simple_class_based_view(): """Test creating class based routers""" - @hug.object.urls('/endpoint', requires=()) - class MyClass(object): + @hug.object.urls("/endpoint", requires=()) + class MyClass(object): @hug.object.get() def my_method(self): - return 'hi there!' + return "hi there!" @hug.object.post() def my_method_two(self): - return 'bye' + return "bye" - assert hug.test.get(api, 'endpoint').data == 'hi there!' - assert hug.test.post(api, 'endpoint').data == 'bye' + assert hug.test.get(api, "endpoint").data == "hi there!" + assert hug.test.post(api, "endpoint").data == "bye" def test_url_inheritance(): """Test creating class based routers""" - @hug.object.urls('/endpoint', requires=(), versions=1) - class MyClass(object): - @hug.object.urls('inherits_base') + @hug.object.urls("/endpoint", requires=(), versions=1) + class MyClass(object): + @hug.object.urls("inherits_base") def my_method(self): - return 'hi there!' + return "hi there!" - @hug.object.urls('/ignores_base') + @hug.object.urls("/ignores_base") def my_method_two(self): - return 'bye' + return "bye" - @hug.object.urls('ignore_version', versions=None) + @hug.object.urls("ignore_version", versions=None) def my_method_three(self): - return 'what version?' + return "what version?" - assert hug.test.get(api, '/v1/endpoint/inherits_base').data == 'hi there!' - assert hug.test.post(api, '/v1/ignores_base').data == 'bye' - assert hug.test.post(api, '/v2/ignores_base').data != 'bye' - assert hug.test.get(api, '/endpoint/ignore_version').data == 'what version?' + assert hug.test.get(api, "/v1/endpoint/inherits_base").data == "hi there!" + assert hug.test.post(api, "/v1/ignores_base").data == "bye" + assert hug.test.post(api, "/v2/ignores_base").data != "bye" + assert hug.test.get(api, "/endpoint/ignore_version").data == "what version?" def test_simple_class_based_method_view(): """Test creating class based routers using method mappings""" + @hug.object.http_methods() class EndPoint(object): - def get(self): - return 'hi there!' + return "hi there!" def post(self): - return 'bye' + return "bye" - assert hug.test.get(api, 'endpoint').data == 'hi there!' - assert hug.test.post(api, 'endpoint').data == 'bye' + assert hug.test.get(api, "endpoint").data == "hi there!" + assert hug.test.post(api, "endpoint").data == "bye" def test_routing_class_based_method_view_with_sub_routing(): """Test creating class based routers using method mappings, then overriding url on sub method""" + @hug.object.http_methods() class EndPoint(object): - def get(self): - return 'hi there!' + return "hi there!" - @hug.object.urls('/home/') + @hug.object.urls("/home/") def post(self): - return 'bye' + return "bye" - assert hug.test.get(api, 'endpoint').data == 'hi there!' - assert hug.test.post(api, 'home').data == 'bye' + assert hug.test.get(api, "endpoint").data == "hi there!" + assert hug.test.post(api, "home").data == "bye" def test_routing_class_with_cli_commands(): """Basic operation test""" - @hug.object(name='git', version='1.0.0') + + @hug.object(name="git", version="1.0.0") class GIT(object): """An example of command like calls via an Object""" @hug.object.cli - def push(self, branch='master'): - return 'Pushing {}'.format(branch) + def push(self, branch="master"): + return "Pushing {}".format(branch) @hug.object.cli - def pull(self, branch='master'): - return 'Pulling {}'.format(branch) + def pull(self, branch="master"): + return "Pulling {}".format(branch) - assert 'token' in hug.test.cli(GIT.push, branch='token') - assert 'another token' in hug.test.cli(GIT.pull, branch='another token') + assert "token" in hug.test.cli(GIT.push, branch="token") + assert "another token" in hug.test.cli(GIT.pull, branch="another token") def test_routing_class_based_method_view_with_cli_routing(): """Test creating class based routers using method mappings exposing cli endpoints""" + @hug.object.http_methods() class EndPoint(object): - @hug.object.cli def get(self): - return 'hi there!' + return "hi there!" def post(self): - return 'bye' + return "bye" - assert hug.test.get(api, 'endpoint').data == 'hi there!' - assert hug.test.post(api, 'endpoint').data == 'bye' - assert hug.test.cli(EndPoint.get) == 'hi there!' + assert hug.test.get(api, "endpoint").data == "hi there!" + assert hug.test.post(api, "endpoint").data == "bye" + assert hug.test.cli(EndPoint.get) == "hi there!" def test_routing_instance(): """Test to ensure its possible to route a class after it is instanciated""" - class EndPoint(object): + class EndPoint(object): @hug.object def one(self): - return 'one' + return "one" @hug.object def two(self): return 2 hug.object.get()(EndPoint()) - assert hug.test.get(api, 'one').data == 'one' - assert hug.test.get(api, 'two').data == 2 + assert hug.test.get(api, "one").data == "one" + assert hug.test.get(api, "two").data == 2 class TestAPIRouter(object): """Test to ensure the API router enables easily reusing all other routing types while routing to an API""" + router = hug.route.API(__name__) def test_route_url(self): """Test to ensure you can dynamically create a URL route attached to a hug API""" - assert self.router.urls('/hi/').route == URLRouter('/hi/', api=api).route + assert self.router.urls("/hi/").route == URLRouter("/hi/", api=api).route def test_route_http(self): """Test to ensure you can dynamically create an HTTP route attached to a hug API""" - assert self.router.http('/hi/').route == URLRouter('/hi/', api=api).route + assert self.router.http("/hi/").route == URLRouter("/hi/", api=api).route def test_method_routes(self): """Test to ensure you can dynamically create an HTTP route attached to a hug API""" for method in hug.HTTP_METHODS: - assert getattr(self.router, method.lower())('/hi/').route['accept'] == (method, ) + assert getattr(self.router, method.lower())("/hi/").route["accept"] == (method,) - assert self.router.get_post('/hi/').route['accept'] == ('GET', 'POST') - assert self.router.put_post('/hi/').route['accept'] == ('PUT', 'POST') + assert self.router.get_post("/hi/").route["accept"] == ("GET", "POST") + assert self.router.put_post("/hi/").route["accept"] == ("PUT", "POST") def test_not_found(self): """Test to ensure you can dynamically create a Not Found route attached to a hug API""" diff --git a/tests/test_routing.py b/tests/test_routing.py index a000c0c7..decc93c8 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -20,325 +20,389 @@ """ import hug -from hug.routing import (CLIRouter, ExceptionRouter, HTTPRouter, InternalValidation, LocalRouter, - NotFoundRouter, Router, SinkRouter, StaticRouter, URLRouter) +from hug.routing import ( + CLIRouter, + ExceptionRouter, + HTTPRouter, + InternalValidation, + LocalRouter, + NotFoundRouter, + Router, + SinkRouter, + StaticRouter, + URLRouter, +) api = hug.API(__name__) class TestRouter(object): """A collection of tests to ensure the base Router object works as expected""" - route = Router(transform='transform', output='output') + + route = Router(transform="transform", output="output") def test_init(self): """Test to ensure the route instanciates as expected""" - assert self.route.route['transform'] == 'transform' - assert self.route.route['output'] == 'output' - assert 'api' not in self.route.route + assert self.route.route["transform"] == "transform" + assert self.route.route["output"] == "output" + assert "api" not in self.route.route def test_output(self): """Test to ensure modifying the output argument has the desired effect""" - new_route = self.route.output('test data', transform='transformed') + new_route = self.route.output("test data", transform="transformed") assert new_route != self.route - assert new_route.route['output'] == 'test data' - assert new_route.route['transform'] == 'transformed' + assert new_route.route["output"] == "test data" + assert new_route.route["transform"] == "transformed" def test_transform(self): """Test to ensure changing the transformation on the fly works as expected""" - new_route = self.route.transform('transformed') + new_route = self.route.transform("transformed") assert new_route != self.route - assert new_route.route['transform'] == 'transformed' + assert new_route.route["transform"] == "transformed" def test_validate(self): """Test to ensure overriding the secondary validation method works as expected""" - assert self.route.validate(str).route['validate'] == str + assert self.route.validate(str).route["validate"] == str def test_api(self): """Test to ensure changing the API associated with the route works as expected""" - new_route = self.route.api('new') + new_route = self.route.api("new") assert new_route != self.route - assert new_route.route['api'] == 'new' + assert new_route.route["api"] == "new" def test_requires(self): """Test to ensure requirements can be added on the fly""" - assert self.route.requires(('values', )).route['requires'] == ('values', ) + assert self.route.requires(("values",)).route["requires"] == ("values",) def test_map_params(self): """Test to ensure it is possible to set param mappings on the routing object""" - assert self.route.map_params(id='user_id').route['map_params'] == {'id': 'user_id'} + assert self.route.map_params(id="user_id").route["map_params"] == {"id": "user_id"} def test_where(self): """Test to ensure `where` can be used to replace all arguments on the fly""" - new_route = self.route.where(transform='transformer', output='outputter') + new_route = self.route.where(transform="transformer", output="outputter") assert new_route != self.route - assert new_route.route['output'] == 'outputter' - assert new_route.route['transform'] == 'transformer' + assert new_route.route["output"] == "outputter" + assert new_route.route["transform"] == "transformer" class TestCLIRouter(TestRouter): """A collection of tests to ensure the CLIRouter object works as expected""" - route = CLIRouter(name='cli', version=1, doc='Hi there!', transform='transform', output='output') + + route = CLIRouter( + name="cli", version=1, doc="Hi there!", transform="transform", output="output" + ) def test_name(self): """Test to ensure the name can be replaced on the fly""" - new_route = self.route.name('new name') + new_route = self.route.name("new name") assert new_route != self.route - assert new_route.route['name'] == 'new name' - assert new_route.route['transform'] == 'transform' - assert new_route.route['output'] == 'output' + assert new_route.route["name"] == "new name" + assert new_route.route["transform"] == "transform" + assert new_route.route["output"] == "output" def test_version(self): """Test to ensure the version can be replaced on the fly""" new_route = self.route.version(2) assert new_route != self.route - assert new_route.route['version'] == 2 - assert new_route.route['transform'] == 'transform' - assert new_route.route['output'] == 'output' + assert new_route.route["version"] == 2 + assert new_route.route["transform"] == "transform" + assert new_route.route["output"] == "output" def test_doc(self): """Test to ensure the documentation can be replaced on the fly""" - new_route = self.route.doc('FAQ') + new_route = self.route.doc("FAQ") assert new_route != self.route - assert new_route.route['doc'] == 'FAQ' - assert new_route.route['transform'] == 'transform' - assert new_route.route['output'] == 'output' + assert new_route.route["doc"] == "FAQ" + assert new_route.route["transform"] == "transform" + assert new_route.route["output"] == "output" class TestInternalValidation(TestRouter): """Collection of tests to ensure the base Router for routes that define internal validation work as expected""" - route = InternalValidation(name='cli', doc='Hi there!', transform='transform', output='output') + + route = InternalValidation(name="cli", doc="Hi there!", transform="transform", output="output") def test_raise_on_invalid(self): """Test to ensure it's possible to set a raise on invalid handler per route""" - assert 'raise_on_invalid' not in self.route.route - assert self.route.raise_on_invalid().route['raise_on_invalid'] + assert "raise_on_invalid" not in self.route.route + assert self.route.raise_on_invalid().route["raise_on_invalid"] def test_on_invalid(self): """Test to ensure on_invalid handler can be changed on the fly""" - assert self.route.on_invalid(str).route['on_invalid'] == str + assert self.route.on_invalid(str).route["on_invalid"] == str def test_output_invalid(self): """Test to ensure output_invalid handler can be changed on the fly""" - assert self.route.output_invalid(hug.output_format.json).route['output_invalid'] == hug.output_format.json + assert ( + self.route.output_invalid(hug.output_format.json).route["output_invalid"] + == hug.output_format.json + ) class TestLocalRouter(TestInternalValidation): """A collection of tests to ensure the LocalRouter object works as expected""" - route = LocalRouter(name='cli', doc='Hi there!', transform='transform', output='output') + + route = LocalRouter(name="cli", doc="Hi there!", transform="transform", output="output") def test_validate(self): """Test to ensure changing wether a local route should validate or not works as expected""" - assert 'skip_validation' not in self.route.route + assert "skip_validation" not in self.route.route route = self.route.validate() - assert 'skip_validation' not in route.route + assert "skip_validation" not in route.route route = self.route.validate(False) - assert 'skip_validation' in route.route + assert "skip_validation" in route.route def test_directives(self): """Test to ensure changing wether a local route should supply directives or not works as expected""" - assert 'skip_directives' not in self.route.route + assert "skip_directives" not in self.route.route route = self.route.directives() - assert 'skip_directives' not in route.route + assert "skip_directives" not in route.route route = self.route.directives(False) - assert 'skip_directives' in route.route + assert "skip_directives" in route.route def test_version(self): """Test to ensure changing the version of a LocalRoute on the fly works""" - assert 'version' not in self.route.route + assert "version" not in self.route.route route = self.route.version(2) - assert 'version' in route.route - assert route.route['version'] == 2 + assert "version" in route.route + assert route.route["version"] == 2 class TestHTTPRouter(TestInternalValidation): """Collection of tests to ensure the base HTTPRouter object works as expected""" - route = HTTPRouter(output='output', versions=(1, ), parse_body=False, transform='transform', requires=('love', ), - parameters=('one', ), defaults={'one': 'value'}, status=200) + + route = HTTPRouter( + output="output", + versions=(1,), + parse_body=False, + transform="transform", + requires=("love",), + parameters=("one",), + defaults={"one": "value"}, + status=200, + ) def test_versions(self): """Test to ensure the supported versions can be replaced on the fly""" - assert self.route.versions(4).route['versions'] == (4, ) + assert self.route.versions(4).route["versions"] == (4,) def test_parse_body(self): """Test to ensure the parsing body flag be flipped on the fly""" - assert self.route.parse_body().route['parse_body'] - assert 'parse_body' not in self.route.parse_body(False).route + assert self.route.parse_body().route["parse_body"] + assert "parse_body" not in self.route.parse_body(False).route def test_requires(self): """Test to ensure requirements can be added on the fly""" - assert self.route.requires(('values', )).route['requires'] == ('love', 'values') + assert self.route.requires(("values",)).route["requires"] == ("love", "values") def test_doesnt_require(self): """Ensure requirements can be selectively removed on the fly""" - assert self.route.doesnt_require('love').route.get('requires', ()) == () - assert self.route.doesnt_require('values').route['requires'] == ('love', ) + assert self.route.doesnt_require("love").route.get("requires", ()) == () + assert self.route.doesnt_require("values").route["requires"] == ("love",) - route = self.route.requires(('values', )) - assert route.doesnt_require('love').route['requires'] == ('values', ) - assert route.doesnt_require('values').route['requires'] == ('love', ) - assert route.doesnt_require(('values', 'love')).route.get('requires', ()) == () + route = self.route.requires(("values",)) + assert route.doesnt_require("love").route["requires"] == ("values",) + assert route.doesnt_require("values").route["requires"] == ("love",) + assert route.doesnt_require(("values", "love")).route.get("requires", ()) == () def test_parameters(self): """Test to ensure the parameters can be replaced on the fly""" - assert self.route.parameters(('one', 'two')).route['parameters'] == ('one', 'two') + assert self.route.parameters(("one", "two")).route["parameters"] == ("one", "two") def test_defaults(self): """Test to ensure the defaults can be replaced on the fly""" - assert self.route.defaults({'one': 'three'}).route['defaults'] == {'one': 'three'} + assert self.route.defaults({"one": "three"}).route["defaults"] == {"one": "three"} def test_status(self): """Test to ensure the default status can be changed on the fly""" - assert self.route.set_status(500).route['status'] == 500 - + assert self.route.set_status(500).route["status"] == 500 def test_response_headers(self): """Test to ensure it's possible to switch out response headers for URL routes on the fly""" - assert self.route.response_headers({'one': 'two'}).route['response_headers'] == {'one': 'two'} + assert self.route.response_headers({"one": "two"}).route["response_headers"] == { + "one": "two" + } def test_add_response_headers(self): """Test to ensure it's possible to add headers on the fly""" - route = self.route.response_headers({'one': 'two'}) - assert route.route['response_headers'] == {'one': 'two'} - assert route.add_response_headers({'two': 'three'}).route['response_headers'] == {'one': 'two', 'two': 'three'} + route = self.route.response_headers({"one": "two"}) + assert route.route["response_headers"] == {"one": "two"} + assert route.add_response_headers({"two": "three"}).route["response_headers"] == { + "one": "two", + "two": "three", + } def test_cache(self): """Test to ensure it's easy to add a cache header on the fly""" - assert self.route.cache().route['response_headers']['cache-control'] == 'public, max-age=31536000' + assert ( + self.route.cache().route["response_headers"]["cache-control"] + == "public, max-age=31536000" + ) def test_allow_origins(self): """Test to ensure it's easy to expose route to other resources""" - test_headers = self.route.allow_origins(methods=('GET', 'POST'), credentials=True, - headers="OPTIONS", max_age=10).route['response_headers'] - assert test_headers['Access-Control-Allow-Origin'] == '*' - assert test_headers['Access-Control-Allow-Methods'] == 'GET, POST' - assert test_headers['Access-Control-Allow-Credentials'] == 'true' - assert test_headers['Access-Control-Allow-Headers'] == 'OPTIONS' - assert test_headers['Access-Control-Max-Age'] == 10 - test_headers = self.route.allow_origins('google.com', methods=('GET', 'POST'), credentials=True, - headers="OPTIONS", max_age=10).route['response_headers'] - assert 'Access-Control-Allow-Origin' not in test_headers - assert test_headers['Access-Control-Allow-Methods'] == 'GET, POST' - assert test_headers['Access-Control-Allow-Credentials'] == 'true' - assert test_headers['Access-Control-Allow-Headers'] == 'OPTIONS' - assert test_headers['Access-Control-Max-Age'] == 10 + test_headers = self.route.allow_origins( + methods=("GET", "POST"), credentials=True, headers="OPTIONS", max_age=10 + ).route["response_headers"] + assert test_headers["Access-Control-Allow-Origin"] == "*" + assert test_headers["Access-Control-Allow-Methods"] == "GET, POST" + assert test_headers["Access-Control-Allow-Credentials"] == "true" + assert test_headers["Access-Control-Allow-Headers"] == "OPTIONS" + assert test_headers["Access-Control-Max-Age"] == 10 + test_headers = self.route.allow_origins( + "google.com", methods=("GET", "POST"), credentials=True, headers="OPTIONS", max_age=10 + ).route["response_headers"] + assert "Access-Control-Allow-Origin" not in test_headers + assert test_headers["Access-Control-Allow-Methods"] == "GET, POST" + assert test_headers["Access-Control-Allow-Credentials"] == "true" + assert test_headers["Access-Control-Allow-Headers"] == "OPTIONS" + assert test_headers["Access-Control-Max-Age"] == 10 class TestStaticRouter(TestHTTPRouter): """Test to ensure that the static router sets up routes correctly""" - route = StaticRouter("/here", requires=('love',), cache=True) - route2 = StaticRouter(("/here", "/there"), api='api', cache={'no_store': True}) + + route = StaticRouter("/here", requires=("love",), cache=True) + route2 = StaticRouter(("/here", "/there"), api="api", cache={"no_store": True}) def test_init(self): """Test to ensure the route instanciates as expected""" - assert self.route.route['urls'] == ("/here", ) - assert self.route2.route['urls'] == ("/here", "/there") - assert self.route2.route['api'] == 'api' + assert self.route.route["urls"] == ("/here",) + assert self.route2.route["urls"] == ("/here", "/there") + assert self.route2.route["api"] == "api" class TestSinkRouter(TestHTTPRouter): """Collection of tests to ensure that the SinkRouter works as expected""" - route = SinkRouter(output='output', versions=(1, ), parse_body=False, transform='transform', - requires=('love', ), parameters=('one', ), defaults={'one': 'value'}) + + route = SinkRouter( + output="output", + versions=(1,), + parse_body=False, + transform="transform", + requires=("love",), + parameters=("one",), + defaults={"one": "value"}, + ) class TestNotFoundRouter(TestHTTPRouter): """Collection of tests to ensure the NotFoundRouter object works as expected""" - route = NotFoundRouter(output='output', versions=(1, ), parse_body=False, transform='transform', - requires=('love', ), parameters=('one', ), defaults={'one': 'value'}) + + route = NotFoundRouter( + output="output", + versions=(1,), + parse_body=False, + transform="transform", + requires=("love",), + parameters=("one",), + defaults={"one": "value"}, + ) class TestExceptionRouter(TestHTTPRouter): """Collection of tests to ensure the ExceptionRouter object works as expected""" - route = ExceptionRouter(Exception, output='output', versions=(1, ), parse_body=False, transform='transform', - requires=('love', ), parameters=('one', ), defaults={'one': 'value'}) + + route = ExceptionRouter( + Exception, + output="output", + versions=(1,), + parse_body=False, + transform="transform", + requires=("love",), + parameters=("one",), + defaults={"one": "value"}, + ) class TestURLRouter(TestHTTPRouter): """Collection of tests to ensure the URLRouter object works as expected""" - route = URLRouter('/here', transform='transform', output='output', requires=('love', )) + + route = URLRouter("/here", transform="transform", output="output", requires=("love",)) def test_urls(self): """Test to ensure the url routes can be replaced on the fly""" - assert self.route.urls('/there').route['urls'] == ('/there', ) + assert self.route.urls("/there").route["urls"] == ("/there",) def test_accept(self): """Test to ensure the accept HTTP METHODs can be replaced on the fly""" - assert self.route.accept('GET').route['accept'] == ('GET', ) + assert self.route.accept("GET").route["accept"] == ("GET",) def test_get(self): """Test to ensure the HTTP METHOD can be set to just GET on the fly""" - assert self.route.get().route['accept'] == ('GET', ) - assert self.route.get('/url').route['urls'] == ('/url', ) + assert self.route.get().route["accept"] == ("GET",) + assert self.route.get("/url").route["urls"] == ("/url",) def test_delete(self): """Test to ensure the HTTP METHOD can be set to just DELETE on the fly""" - assert self.route.delete().route['accept'] == ('DELETE', ) - assert self.route.delete('/url').route['urls'] == ('/url', ) + assert self.route.delete().route["accept"] == ("DELETE",) + assert self.route.delete("/url").route["urls"] == ("/url",) def test_post(self): """Test to ensure the HTTP METHOD can be set to just POST on the fly""" - assert self.route.post().route['accept'] == ('POST', ) - assert self.route.post('/url').route['urls'] == ('/url', ) + assert self.route.post().route["accept"] == ("POST",) + assert self.route.post("/url").route["urls"] == ("/url",) def test_put(self): """Test to ensure the HTTP METHOD can be set to just PUT on the fly""" - assert self.route.put().route['accept'] == ('PUT', ) - assert self.route.put('/url').route['urls'] == ('/url', ) + assert self.route.put().route["accept"] == ("PUT",) + assert self.route.put("/url").route["urls"] == ("/url",) def test_trace(self): """Test to ensure the HTTP METHOD can be set to just TRACE on the fly""" - assert self.route.trace().route['accept'] == ('TRACE', ) - assert self.route.trace('/url').route['urls'] == ('/url', ) + assert self.route.trace().route["accept"] == ("TRACE",) + assert self.route.trace("/url").route["urls"] == ("/url",) def test_patch(self): """Test to ensure the HTTP METHOD can be set to just PATCH on the fly""" - assert self.route.patch().route['accept'] == ('PATCH', ) - assert self.route.patch('/url').route['urls'] == ('/url', ) + assert self.route.patch().route["accept"] == ("PATCH",) + assert self.route.patch("/url").route["urls"] == ("/url",) def test_options(self): """Test to ensure the HTTP METHOD can be set to just OPTIONS on the fly""" - assert self.route.options().route['accept'] == ('OPTIONS', ) - assert self.route.options('/url').route['urls'] == ('/url', ) + assert self.route.options().route["accept"] == ("OPTIONS",) + assert self.route.options("/url").route["urls"] == ("/url",) def test_head(self): """Test to ensure the HTTP METHOD can be set to just HEAD on the fly""" - assert self.route.head().route['accept'] == ('HEAD', ) - assert self.route.head('/url').route['urls'] == ('/url', ) + assert self.route.head().route["accept"] == ("HEAD",) + assert self.route.head("/url").route["urls"] == ("/url",) def test_connect(self): """Test to ensure the HTTP METHOD can be set to just CONNECT on the fly""" - assert self.route.connect().route['accept'] == ('CONNECT', ) - assert self.route.connect('/url').route['urls'] == ('/url', ) + assert self.route.connect().route["accept"] == ("CONNECT",) + assert self.route.connect("/url").route["urls"] == ("/url",) def test_call(self): """Test to ensure the HTTP METHOD can be set to accept all on the fly""" - assert self.route.call().route['accept'] == hug.HTTP_METHODS + assert self.route.call().route["accept"] == hug.HTTP_METHODS def test_http(self): """Test to ensure the HTTP METHOD can be set to accept all on the fly""" - assert self.route.http().route['accept'] == hug.HTTP_METHODS + assert self.route.http().route["accept"] == hug.HTTP_METHODS def test_get_post(self): """Test to ensure the HTTP METHOD can be set to GET & POST in one call""" - return self.route.get_post().route['accept'] == ('GET', 'POST') + return self.route.get_post().route["accept"] == ("GET", "POST") def test_put_post(self): """Test to ensure the HTTP METHOD can be set to PUT & POST in one call""" - return self.route.put_post().route['accept'] == ('PUT', 'POST') + return self.route.put_post().route["accept"] == ("PUT", "POST") def test_examples(self): """Test to ensure examples can be modified on the fly""" - assert self.route.examples('none').route['examples'] == ('none', ) + assert self.route.examples("none").route["examples"] == ("none",) def test_prefixes(self): """Test to ensure adding prefixes works as expected""" - assert self.route.prefixes('/js/').route['prefixes'] == ('/js/', ) + assert self.route.prefixes("/js/").route["prefixes"] == ("/js/",) def test_suffixes(self): """Test to ensure setting suffixes works as expected""" - assert self.route.suffixes('.js', '.xml').route['suffixes'] == ('.js', '.xml') + assert self.route.suffixes(".js", ".xml").route["suffixes"] == (".js", ".xml") diff --git a/tests/test_store.py b/tests/test_store.py index 2e0eb332..1ae04fd5 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -24,18 +24,13 @@ from hug.exceptions import StoreKeyNotFound from hug.store import InMemoryStore -stores_to_test = [ - InMemoryStore() -] +stores_to_test = [InMemoryStore()] -@pytest.mark.parametrize('store', stores_to_test) +@pytest.mark.parametrize("store", stores_to_test) def test_stores_generically(store): - key = 'test-key' - data = { - 'user': 'foo', - 'authenticated': False - } + key = "test-key" + data = {"user": "foo", "authenticated": False} # Key should not exist assert not store.exists(key) @@ -47,7 +42,7 @@ def test_stores_generically(store): # Expect exception if unknown session key was requested with pytest.raises(StoreKeyNotFound): - store.get('unknown') + store.get("unknown") # Delete key store.delete(key) diff --git a/tests/test_test.py b/tests/test_test.py index 74998646..b07539b4 100644 --- a/tests/test_test.py +++ b/tests/test_test.py @@ -28,13 +28,14 @@ def test_cli(): """Test to ensure the CLI tester works as intended to allow testing CLI endpoints""" + @hug.cli() def my_cli_function(): - return 'Hello' + return "Hello" - assert hug.test.cli(my_cli_function) == 'Hello' - assert hug.test.cli('my_cli_function', api=api) == 'Hello' + assert hug.test.cli(my_cli_function) == "Hello" + assert hug.test.cli("my_cli_function", api=api) == "Hello" # Shouldn't be able to specify both api and module. with pytest.raises(ValueError): - assert hug.test.cli('my_method', api=api, module=hug) + assert hug.test.cli("my_method", api=api, module=hug) diff --git a/tests/test_transform.py b/tests/test_transform.py index 2e6c2fcd..d5f857dc 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -24,58 +24,59 @@ def test_content_type(): """Test to ensure the transformer used can change based on the provided content-type""" - transformer = hug.transform.content_type({'application/json': int, 'text/plain': str}) + transformer = hug.transform.content_type({"application/json": int, "text/plain": str}) class FakeRequest(object): - content_type = 'application/json' + content_type = "application/json" request = FakeRequest() - assert transformer('1', request) == 1 + assert transformer("1", request) == 1 - request.content_type = 'text/plain' - assert transformer(2, request) == '2' + request.content_type = "text/plain" + assert transformer(2, request) == "2" - request.content_type = 'undefined' - transformer({'data': 'value'}, request) == {'data': 'value'} + request.content_type = "undefined" + transformer({"data": "value"}, request) == {"data": "value"} def test_suffix(): """Test to ensure transformer content based on the end suffix of the URL works as expected""" - transformer = hug.transform.suffix({'.js': int, '.txt': str}) + transformer = hug.transform.suffix({".js": int, ".txt": str}) class FakeRequest(object): - path = 'hey.js' + path = "hey.js" request = FakeRequest() - assert transformer('1', request) == 1 + assert transformer("1", request) == 1 - request.path = 'hey.txt' - assert transformer(2, request) == '2' + request.path = "hey.txt" + assert transformer(2, request) == "2" - request.path = 'hey.undefined' - transformer({'data': 'value'}, request) == {'data': 'value'} + request.path = "hey.undefined" + transformer({"data": "value"}, request) == {"data": "value"} def test_prefix(): """Test to ensure transformer content based on the end prefix of the URL works as expected""" - transformer = hug.transform.prefix({'js/': int, 'txt/': str}) + transformer = hug.transform.prefix({"js/": int, "txt/": str}) class FakeRequest(object): - path = 'js/hey' + path = "js/hey" request = FakeRequest() - assert transformer('1', request) == 1 + assert transformer("1", request) == 1 - request.path = 'txt/hey' - assert transformer(2, request) == '2' + request.path = "txt/hey" + assert transformer(2, request) == "2" - request.path = 'hey.undefined' - transformer({'data': 'value'}, request) == {'data': 'value'} + request.path = "hey.undefined" + transformer({"data": "value"}, request) == {"data": "value"} def test_all(): """Test to ensure transform.all allows chaining multiple transformations as expected""" + def annotate(data, response): - return {'Text': data} + return {"Text": data} - assert hug.transform.all(str, annotate)(1, response='hi') == {'Text': '1'} + assert hug.transform.all(str, annotate)(1, response="hi") == {"Text": "1"} diff --git a/tests/test_types.py b/tests/test_types.py index 4c3df1e7..b8dcaacf 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -38,234 +38,249 @@ def test_type(): """Test to ensure the abstract Type object can't be used""" with pytest.raises(NotImplementedError): - hug.types.Type()('value') + hug.types.Type()("value") def test_number(): """Tests that hug's number type correctly converts and validates input""" - assert hug.types.number('1') == 1 + assert hug.types.number("1") == 1 assert hug.types.number(1) == 1 with pytest.raises(ValueError): - hug.types.number('bacon') + hug.types.number("bacon") def test_range(): """Tests that hug's range type successfully handles ranges of numbers""" - assert hug.types.in_range(1, 10)('1') == 1 + assert hug.types.in_range(1, 10)("1") == 1 assert hug.types.in_range(1, 10)(1) == 1 - assert '1' in hug.types.in_range(1, 10).__doc__ + assert "1" in hug.types.in_range(1, 10).__doc__ with pytest.raises(ValueError): - hug.types.in_range(1, 10)('bacon') + hug.types.in_range(1, 10)("bacon") with pytest.raises(ValueError): - hug.types.in_range(1, 10)('15') + hug.types.in_range(1, 10)("15") with pytest.raises(ValueError): hug.types.in_range(1, 10)(-34) def test_less_than(): """Tests that hug's less than type successfully limits the values passed in""" - assert hug.types.less_than(10)('1') == 1 + assert hug.types.less_than(10)("1") == 1 assert hug.types.less_than(10)(1) == 1 assert hug.types.less_than(10)(-10) == -10 - assert '10' in hug.types.less_than(10).__doc__ + assert "10" in hug.types.less_than(10).__doc__ with pytest.raises(ValueError): assert hug.types.less_than(10)(10) def test_greater_than(): """Tests that hug's greater than type succefully limis the values passed in""" - assert hug.types.greater_than(10)('11') == 11 + assert hug.types.greater_than(10)("11") == 11 assert hug.types.greater_than(10)(11) == 11 assert hug.types.greater_than(10)(1000) == 1000 - assert '10' in hug.types.greater_than(10).__doc__ + assert "10" in hug.types.greater_than(10).__doc__ with pytest.raises(ValueError): assert hug.types.greater_than(10)(9) def test_multiple(): """Tests that hug's multile type correctly forces values to come back as lists, but not lists of lists""" - assert hug.types.multiple('value') == ['value'] - assert hug.types.multiple(['value1', 'value2']) == ['value1', 'value2'] + assert hug.types.multiple("value") == ["value"] + assert hug.types.multiple(["value1", "value2"]) == ["value1", "value2"] def test_delimited_list(): """Test to ensure hug's custom delimited list type function works as expected""" - assert hug.types.delimited_list(',')('value1,value2') == ['value1', 'value2'] - assert hug.types.DelimitedList[int](',')('1,2') == [1, 2] - assert hug.types.delimited_list(',')(['value1', 'value2']) == ['value1', 'value2'] - assert hug.types.delimited_list('|-|')('value1|-|value2|-|value3,value4') == ['value1', 'value2', 'value3,value4'] - assert ',' in hug.types.delimited_list(',').__doc__ + assert hug.types.delimited_list(",")("value1,value2") == ["value1", "value2"] + assert hug.types.DelimitedList[int](",")("1,2") == [1, 2] + assert hug.types.delimited_list(",")(["value1", "value2"]) == ["value1", "value2"] + assert hug.types.delimited_list("|-|")("value1|-|value2|-|value3,value4") == [ + "value1", + "value2", + "value3,value4", + ] + assert "," in hug.types.delimited_list(",").__doc__ def test_comma_separated_list(): """Tests that hug's comma separated type correctly converts into a Python list""" - assert hug.types.comma_separated_list('value') == ['value'] - assert hug.types.comma_separated_list('value1,value2') == ['value1', 'value2'] + assert hug.types.comma_separated_list("value") == ["value"] + assert hug.types.comma_separated_list("value1,value2") == ["value1", "value2"] def test_float_number(): """Tests to ensure the float type correctly allows floating point values""" - assert hug.types.float_number('1.1') == 1.1 - assert hug.types.float_number('1') == float(1) + assert hug.types.float_number("1.1") == 1.1 + assert hug.types.float_number("1") == float(1) assert hug.types.float_number(1.1) == 1.1 with pytest.raises(ValueError): - hug.types.float_number('bacon') + hug.types.float_number("bacon") def test_decimal(): """Tests to ensure the decimal type correctly allows decimal values""" - assert hug.types.decimal('1.1') == Decimal('1.1') - assert hug.types.decimal('1') == Decimal('1') + assert hug.types.decimal("1.1") == Decimal("1.1") + assert hug.types.decimal("1") == Decimal("1") assert hug.types.decimal(1.1) == Decimal(1.1) with pytest.raises(ValueError): - hug.types.decimal('bacon') + hug.types.decimal("bacon") def test_boolean(): """Test to ensure the custom boolean type correctly supports boolean conversion""" - assert hug.types.boolean('1') - assert hug.types.boolean('T') - assert not hug.types.boolean('') - assert hug.types.boolean('False') + assert hug.types.boolean("1") + assert hug.types.boolean("T") + assert not hug.types.boolean("") + assert hug.types.boolean("False") assert not hug.types.boolean(False) def test_mapping(): """Test to ensure the mapping type works as expected""" - mapping_type = hug.types.mapping({'n': None, 'l': [], 's': set()}) - assert mapping_type('n') is None - assert mapping_type('l') == [] - assert mapping_type('s') == set() - assert 'n' in mapping_type.__doc__ + mapping_type = hug.types.mapping({"n": None, "l": [], "s": set()}) + assert mapping_type("n") is None + assert mapping_type("l") == [] + assert mapping_type("s") == set() + assert "n" in mapping_type.__doc__ with pytest.raises(KeyError): - mapping_type('bacon') + mapping_type("bacon") def test_smart_boolean(): """Test to ensure that the smart boolean type works as expected""" - assert hug.types.smart_boolean('true') - assert hug.types.smart_boolean('t') - assert hug.types.smart_boolean('1') + assert hug.types.smart_boolean("true") + assert hug.types.smart_boolean("t") + assert hug.types.smart_boolean("1") assert hug.types.smart_boolean(1) - assert not hug.types.smart_boolean('') - assert not hug.types.smart_boolean('false') - assert not hug.types.smart_boolean('f') - assert not hug.types.smart_boolean('0') + assert not hug.types.smart_boolean("") + assert not hug.types.smart_boolean("false") + assert not hug.types.smart_boolean("f") + assert not hug.types.smart_boolean("0") assert not hug.types.smart_boolean(0) assert hug.types.smart_boolean(True) assert not hug.types.smart_boolean(None) assert not hug.types.smart_boolean(False) with pytest.raises(KeyError): - hug.types.smart_boolean('bacon') + hug.types.smart_boolean("bacon") def test_text(): """Tests that hug's text validator correctly handles basic values""" - assert hug.types.text('1') == '1' - assert hug.types.text(1) == '1' - assert hug.types.text('text') == 'text' + assert hug.types.text("1") == "1" + assert hug.types.text(1) == "1" + assert hug.types.text("text") == "text" with pytest.raises(ValueError): - hug.types.text(['one', 'two']) + hug.types.text(["one", "two"]) + def test_uuid(): """Tests that hug's text validator correctly handles UUID values Examples were taken from https://docs.python.org/3/library/uuid.html""" - assert hug.types.uuid('{12345678-1234-5678-1234-567812345678}') == UUID('12345678-1234-5678-1234-567812345678') - assert hug.types.uuid('12345678-1234-5678-1234-567812345678') == UUID('12345678123456781234567812345678') - assert hug.types.uuid('12345678123456781234567812345678') == UUID('12345678-1234-5678-1234-567812345678') - assert hug.types.uuid('urn:uuid:12345678-1234-5678-1234-567812345678') == \ - UUID('12345678-1234-5678-1234-567812345678') + assert hug.types.uuid("{12345678-1234-5678-1234-567812345678}") == UUID( + "12345678-1234-5678-1234-567812345678" + ) + assert hug.types.uuid("12345678-1234-5678-1234-567812345678") == UUID( + "12345678123456781234567812345678" + ) + assert hug.types.uuid("12345678123456781234567812345678") == UUID( + "12345678-1234-5678-1234-567812345678" + ) + assert hug.types.uuid("urn:uuid:12345678-1234-5678-1234-567812345678") == UUID( + "12345678-1234-5678-1234-567812345678" + ) with pytest.raises(ValueError): hug.types.uuid(1) with pytest.raises(ValueError): # Invalid HEX character - hug.types.uuid('12345678-1234-5678-1234-56781234567G') + hug.types.uuid("12345678-1234-5678-1234-56781234567G") with pytest.raises(ValueError): # One character added - hug.types.uuid('12345678-1234-5678-1234-5678123456781') + hug.types.uuid("12345678-1234-5678-1234-5678123456781") with pytest.raises(ValueError): # One character removed - hug.types.uuid('12345678-1234-5678-1234-56781234567') - + hug.types.uuid("12345678-1234-5678-1234-56781234567") def test_length(): """Tests that hug's length type successfully handles a length range""" - assert hug.types.length(1, 10)('bacon') == 'bacon' - assert hug.types.length(1, 10)(42) == '42' - assert '42' in hug.types.length(1, 42).__doc__ + assert hug.types.length(1, 10)("bacon") == "bacon" + assert hug.types.length(1, 10)(42) == "42" + assert "42" in hug.types.length(1, 42).__doc__ with pytest.raises(ValueError): - hug.types.length(1, 10)('bacon is the greatest food known to man') + hug.types.length(1, 10)("bacon is the greatest food known to man") with pytest.raises(ValueError): - hug.types.length(1, 10)('') + hug.types.length(1, 10)("") with pytest.raises(ValueError): - hug.types.length(1, 10)('bacon is th') + hug.types.length(1, 10)("bacon is th") def test_shorter_than(): """Tests that hug's shorter than type successfully limits the values passed in""" - assert hug.types.shorter_than(10)('hi there') == 'hi there' - assert hug.types.shorter_than(10)(1) == '1' - assert hug.types.shorter_than(10)('') == '' - assert '10' in hug.types.shorter_than(10).__doc__ + assert hug.types.shorter_than(10)("hi there") == "hi there" + assert hug.types.shorter_than(10)(1) == "1" + assert hug.types.shorter_than(10)("") == "" + assert "10" in hug.types.shorter_than(10).__doc__ with pytest.raises(ValueError): - assert hug.types.shorter_than(10)('there is quite a bit of text here, in fact way more than allowed') + assert hug.types.shorter_than(10)( + "there is quite a bit of text here, in fact way more than allowed" + ) def test_longer_than(): """Tests that hug's greater than type succefully limis the values passed in""" - assert hug.types.longer_than(10)('quite a bit of text here should be') == 'quite a bit of text here should be' - assert hug.types.longer_than(10)(12345678910) == '12345678910' - assert hug.types.longer_than(10)(100123456789100) == '100123456789100' - assert '10' in hug.types.longer_than(10).__doc__ + assert ( + hug.types.longer_than(10)("quite a bit of text here should be") + == "quite a bit of text here should be" + ) + assert hug.types.longer_than(10)(12345678910) == "12345678910" + assert hug.types.longer_than(10)(100123456789100) == "100123456789100" + assert "10" in hug.types.longer_than(10).__doc__ with pytest.raises(ValueError): - assert hug.types.longer_than(10)('short') + assert hug.types.longer_than(10)("short") def test_cut_off(): """Test to ensure that hug's cut_off type works as expected""" - assert hug.types.cut_off(10)('text') == 'text' - assert hug.types.cut_off(10)(10) == '10' - assert hug.types.cut_off(10)('some really long text') == 'some reall' - assert '10' in hug.types.cut_off(10).__doc__ + assert hug.types.cut_off(10)("text") == "text" + assert hug.types.cut_off(10)(10) == "10" + assert hug.types.cut_off(10)("some really long text") == "some reall" + assert "10" in hug.types.cut_off(10).__doc__ def test_inline_dictionary(): """Tests that inline dictionary values are correctly handled""" int_dict = hug.types.InlineDictionary[int, int]() - assert int_dict('1:2') == {1: 2} - assert int_dict('1:2|3:4') == {1: 2, 3: 4} - assert hug.types.inline_dictionary('1:2') == {'1': '2'} - assert hug.types.inline_dictionary('1:2|3:4') == {'1': '2', '3': '4'} + assert int_dict("1:2") == {1: 2} + assert int_dict("1:2|3:4") == {1: 2, 3: 4} + assert hug.types.inline_dictionary("1:2") == {"1": "2"} + assert hug.types.inline_dictionary("1:2|3:4") == {"1": "2", "3": "4"} with pytest.raises(ValueError): - hug.types.inline_dictionary('1') + hug.types.inline_dictionary("1") int_dict = hug.types.InlineDictionary[int]() - assert int_dict('1:2') == {1: '2'} + assert int_dict("1:2") == {1: "2"} int_dict = hug.types.InlineDictionary[int, int, int]() - assert int_dict('1:2') == {1: 2} - + assert int_dict("1:2") == {1: 2} def test_one_of(): """Tests that hug allows limiting a value to one of a list of values""" - assert hug.types.one_of(('bacon', 'sausage', 'pancakes'))('bacon') == 'bacon' - assert hug.types.one_of(['bacon', 'sausage', 'pancakes'])('sausage') == 'sausage' - assert hug.types.one_of({'bacon', 'sausage', 'pancakes'})('pancakes') == 'pancakes' - assert 'bacon' in hug.types.one_of({'bacon', 'sausage', 'pancakes'}).__doc__ + assert hug.types.one_of(("bacon", "sausage", "pancakes"))("bacon") == "bacon" + assert hug.types.one_of(["bacon", "sausage", "pancakes"])("sausage") == "sausage" + assert hug.types.one_of({"bacon", "sausage", "pancakes"})("pancakes") == "pancakes" + assert "bacon" in hug.types.one_of({"bacon", "sausage", "pancakes"}).__doc__ with pytest.raises(KeyError): - hug.types.one_of({'bacon', 'sausage', 'pancakes'})('syrup') + hug.types.one_of({"bacon", "sausage", "pancakes"})("syrup") def test_accept(): """Tests to ensure the accept type wrapper works as expected""" custom_converter = lambda value: value + " converted" - custom_type = hug.types.accept(custom_converter, 'A string Value') + custom_type = hug.types.accept(custom_converter, "A string Value") with pytest.raises(TypeError): custom_type(1) @@ -273,8 +288,8 @@ def test_accept(): def test_accept_custom_exception_text(): """Tests to ensure it's easy to custom the exception text using the accept wrapper""" custom_converter = lambda value: value + " converted" - custom_type = hug.types.accept(custom_converter, 'A string Value', 'Error occurred') - assert custom_type('bacon') == 'bacon converted' + custom_type = hug.types.accept(custom_converter, "A string Value", "Error occurred") + assert custom_type("bacon") == "bacon converted" with pytest.raises(ValueError): custom_type(1) @@ -282,34 +297,38 @@ def test_accept_custom_exception_text(): def test_accept_custom_exception_handlers(): """Tests to ensure it's easy to custom the exception text using the accept wrapper""" custom_converter = lambda value: (str(int(value)) if value else value) + " converted" - custom_type = hug.types.accept(custom_converter, 'A string Value', exception_handlers={TypeError: '0 provided'}) - assert custom_type('1') == '1 converted' + custom_type = hug.types.accept( + custom_converter, "A string Value", exception_handlers={TypeError: "0 provided"} + ) + assert custom_type("1") == "1 converted" with pytest.raises(ValueError): - custom_type('bacon') + custom_type("bacon") with pytest.raises(ValueError): custom_type(0) - custom_type = hug.types.accept(custom_converter, 'A string Value', exception_handlers={TypeError: KeyError}) + custom_type = hug.types.accept( + custom_converter, "A string Value", exception_handlers={TypeError: KeyError} + ) with pytest.raises(KeyError): custom_type(0) def test_json(): """Test to ensure that the json type correctly handles url encoded json, as well as direct json""" - assert hug.types.json({'this': 'works'}) == {'this': 'works'} - assert hug.types.json(json.dumps({'this': 'works'})) == {'this': 'works'} + assert hug.types.json({"this": "works"}) == {"this": "works"} + assert hug.types.json(json.dumps({"this": "works"})) == {"this": "works"} with pytest.raises(ValueError): - hug.types.json('Invalid JSON') + hug.types.json("Invalid JSON") def test_multi(): """Test to ensure that the multi type correctly handles a variety of value types""" multi_type = hug.types.multi(hug.types.json, hug.types.smart_boolean) - assert multi_type({'this': 'works'}) == {'this': 'works'} - assert multi_type(json.dumps({'this': 'works'})) == {'this': 'works'} - assert multi_type('t') + assert multi_type({"this": "works"}) == {"this": "works"} + assert multi_type(json.dumps({"this": "works"})) == {"this": "works"} + assert multi_type("t") with pytest.raises(ValueError): - multi_type('Bacon!') + multi_type("Bacon!") def test_chain(): @@ -331,9 +350,11 @@ def test_nullable(): def test_schema_type(): """Test hug's complex schema types""" + class User(hug.types.Schema): username = hug.types.text password = hug.types.Chain(hug.types.text, hug.types.LongerThan(10)) + user_one = User({"username": "brandon", "password": "password123"}) user_two = User(user_one) with pytest.raises(ValueError): @@ -357,78 +378,83 @@ class User(hug.types.Schema): def test_marshmallow_schema(): """Test hug's marshmallow schema support""" + class UserSchema(Schema): name = fields.Int() schema_type = hug.types.MarshmallowInputSchema(UserSchema()) assert schema_type({"name": 23}, {}) == {"name": 23} assert schema_type("""{"name": 23}""", {}) == {"name": 23} - assert schema_type.__doc__ == 'UserSchema' + assert schema_type.__doc__ == "UserSchema" with pytest.raises(InvalidTypeData): schema_type({"name": "test"}, {}) schema_type = hug.types.MarshmallowReturnSchema(UserSchema()) assert schema_type({"name": 23}) == {"name": 23} - assert schema_type.__doc__ == 'UserSchema' + assert schema_type.__doc__ == "UserSchema" with pytest.raises(InvalidTypeData): schema_type({"name": "test"}) def test_create_type(): """Test hug's new type creation decorator works as expected""" - @hug.type(extend=hug.types.text, exception_handlers={TypeError: ValueError, LookupError: 'Hi!'}, - error_text='Invalid') + + @hug.type( + extend=hug.types.text, + exception_handlers={TypeError: ValueError, LookupError: "Hi!"}, + error_text="Invalid", + ) def prefixed_string(value): - if value == 'hi': - raise TypeError('Repeat of prefix') - elif value == 'bye': - raise LookupError('Never say goodbye!') - elif value == '1+1': - raise ArithmeticError('Testing different error types') - return 'hi-' + value - - assert prefixed_string('there') == 'hi-there' + if value == "hi": + raise TypeError("Repeat of prefix") + elif value == "bye": + raise LookupError("Never say goodbye!") + elif value == "1+1": + raise ArithmeticError("Testing different error types") + return "hi-" + value + + assert prefixed_string("there") == "hi-there" with pytest.raises(ValueError): prefixed_string([]) with pytest.raises(ValueError): - prefixed_string('hi') + prefixed_string("hi") with pytest.raises(ValueError): - prefixed_string('bye') + prefixed_string("bye") @hug.type(extend=hug.types.text, exception_handlers={TypeError: ValueError}) def prefixed_string(value): - if value == '1+1': - raise ArithmeticError('Testing different error types') - return 'hi-' + value + if value == "1+1": + raise ArithmeticError("Testing different error types") + return "hi-" + value with pytest.raises(ArithmeticError): - prefixed_string('1+1') + prefixed_string("1+1") @hug.type(extend=hug.types.text) def prefixed_string(value): - return 'hi-' + value + return "hi-" + value - assert prefixed_string('there') == 'hi-there' + assert prefixed_string("there") == "hi-there" @hug.type(extend=hug.types.one_of) def numbered(value): return int(value) - assert numbered(['1', '2', '3'])('1') == 1 + assert numbered(["1", "2", "3"])("1") == 1 def test_marshmallow_custom_context(): - custom_context = dict(context='global', factory=0, delete=0, marshmallow=0) + custom_context = dict(context="global", factory=0, delete=0, marshmallow=0) @hug.context_factory(apply_globally=True) def create_context(*args, **kwargs): - custom_context['factory'] += 1 + custom_context["factory"] += 1 return custom_context @hug.delete_context(apply_globally=True) def delete_context(context, *args, **kwargs): assert context == custom_context - custom_context['delete'] += 1 + custom_context["delete"] += 1 class MarshmallowContextSchema(Schema): name = fields.String() @@ -436,20 +462,20 @@ class MarshmallowContextSchema(Schema): @validates_schema def check_context(self, data): assert self.context == custom_context - self.context['marshmallow'] += 1 + self.context["marshmallow"] += 1 @hug.get() def made_up_hello(test: MarshmallowContextSchema()): - return 'hi' + return "hi" - assert hug.test.get(api, '/made_up_hello', {'test': {'name': 'test'}}).data == 'hi' - assert custom_context['factory'] == 1 - assert custom_context['delete'] == 1 - assert custom_context['marshmallow'] == 1 + assert hug.test.get(api, "/made_up_hello", {"test": {"name": "test"}}).data == "hi" + assert custom_context["factory"] == 1 + assert custom_context["delete"] == 1 + assert custom_context["marshmallow"] == 1 def test_extending_types_with_context_with_no_error_messages(): - custom_context = dict(context='global', the_only_right_number=42) + custom_context = dict(context="global", the_only_right_number=42) @hug.context_factory() def create_context(*args, **kwargs): @@ -462,39 +488,39 @@ def delete_context(*args, **kwargs): @hug.type(chain=True, extend=hug.types.number) def check_if_positive(value): if value < 0: - raise ValueError('Not positive') + raise ValueError("Not positive") return value @hug.type(chain=True, extend=check_if_positive, accept_context=True) def check_if_near_the_right_number(value, context): - the_only_right_number = context['the_only_right_number'] + the_only_right_number = context["the_only_right_number"] if value not in [ the_only_right_number - 1, the_only_right_number, the_only_right_number + 1, ]: - raise ValueError('Not near the right number') + raise ValueError("Not near the right number") return value @hug.type(chain=True, extend=check_if_near_the_right_number, accept_context=True) def check_if_the_only_right_number(value, context): - if value != context['the_only_right_number']: - raise ValueError('Not the right number') + if value != context["the_only_right_number"]: + raise ValueError("Not the right number") return value @hug.type(chain=False, extend=hug.types.number, accept_context=True) def check_if_string_has_right_value(value, context): - if str(context['the_only_right_number']) not in value: - raise ValueError('The value does not contain the only right number') + if str(context["the_only_right_number"]) not in value: + raise ValueError("The value does not contain the only right number") return value @hug.type(chain=False, extend=hug.types.number) def simple_check(value): - if value != 'simple': - raise ValueError('This is not simple') + if value != "simple": + raise ValueError("This is not simple") return value - @hug.get('/check_the_types') + @hug.get("/check_the_types") def check_the_types( first: check_if_positive, second: check_if_near_the_right_number, @@ -502,66 +528,46 @@ def check_the_types( forth: check_if_string_has_right_value, fifth: simple_check, ): - return 'hi' + return "hi" test_cases = [ + ((42, 42, 42, "42", "simple"), (None, None, None, None, None)), + ((43, 43, 43, "42", "simple"), (None, None, "Not the right number", None, None)), ( - (42, 42, 42, '42', 'simple',), - ( - None, - None, - None, - None, - None, - ), - ), - ( - (43, 43, 43, '42', 'simple',), - ( - None, - None, - 'Not the right number', - None, - None, - ), - ), - ( - (40, 40, 40, '42', 'simple',), - ( - None, - 'Not near the right number', - 'Not near the right number', - None, - None, - ), + (40, 40, 40, "42", "simple"), + (None, "Not near the right number", "Not near the right number", None, None), ), ( - (-42, -42, -42, '53', 'not_simple',), + (-42, -42, -42, "53", "not_simple"), ( - 'Not positive', - 'Not positive', - 'Not positive', - 'The value does not contain the only right number', - 'This is not simple', + "Not positive", + "Not positive", + "Not positive", + "The value does not contain the only right number", + "This is not simple", ), ), ] for provided_values, expected_results in test_cases: - response = hug.test.get(api, '/check_the_types', **{ - 'first': provided_values[0], - 'second': provided_values[1], - 'third': provided_values[2], - 'forth': provided_values[3], - 'fifth': provided_values[4] - }) - if response.data == 'hi': + response = hug.test.get( + api, + "/check_the_types", + **{ + "first": provided_values[0], + "second": provided_values[1], + "third": provided_values[2], + "forth": provided_values[3], + "fifth": provided_values[4], + } + ) + if response.data == "hi": errors = (None, None, None, None, None) else: errors = [] - for key in ['first', 'second', 'third', 'forth', 'fifth']: - if key in response.data['errors']: - errors.append(response.data['errors'][key]) + for key in ["first", "second", "third", "forth", "fifth"]: + if key in response.data["errors"]: + errors.append(response.data["errors"][key]) else: errors.append(None) errors = tuple(errors) @@ -569,7 +575,7 @@ def check_the_types( def test_extending_types_with_context_with_error_messages(): - custom_context = dict(context='global', the_only_right_number=42) + custom_context = dict(context="global", the_only_right_number=42) @hug.context_factory() def create_context(*args, **kwargs): @@ -579,42 +585,44 @@ def create_context(*args, **kwargs): def delete_context(*args, **kwargs): pass - @hug.type(chain=True, extend=hug.types.number, error_text='error 1') + @hug.type(chain=True, extend=hug.types.number, error_text="error 1") def check_if_positive(value): if value < 0: - raise ValueError('Not positive') + raise ValueError("Not positive") return value - @hug.type(chain=True, extend=check_if_positive, accept_context=True, error_text='error 2') + @hug.type(chain=True, extend=check_if_positive, accept_context=True, error_text="error 2") def check_if_near_the_right_number(value, context): - the_only_right_number = context['the_only_right_number'] + the_only_right_number = context["the_only_right_number"] if value not in [ the_only_right_number - 1, the_only_right_number, the_only_right_number + 1, ]: - raise ValueError('Not near the right number') + raise ValueError("Not near the right number") return value - @hug.type(chain=True, extend=check_if_near_the_right_number, accept_context=True, error_text='error 3') + @hug.type( + chain=True, extend=check_if_near_the_right_number, accept_context=True, error_text="error 3" + ) def check_if_the_only_right_number(value, context): - if value != context['the_only_right_number']: - raise ValueError('Not the right number') + if value != context["the_only_right_number"]: + raise ValueError("Not the right number") return value - @hug.type(chain=False, extend=hug.types.number, accept_context=True, error_text='error 4') + @hug.type(chain=False, extend=hug.types.number, accept_context=True, error_text="error 4") def check_if_string_has_right_value(value, context): - if str(context['the_only_right_number']) not in value: - raise ValueError('The value does not contain the only right number') + if str(context["the_only_right_number"]) not in value: + raise ValueError("The value does not contain the only right number") return value - @hug.type(chain=False, extend=hug.types.number, error_text='error 5') + @hug.type(chain=False, extend=hug.types.number, error_text="error 5") def simple_check(value): - if value != 'simple': - raise ValueError('This is not simple') + if value != "simple": + raise ValueError("This is not simple") return value - @hug.get('/check_the_types') + @hug.get("/check_the_types") def check_the_types( first: check_if_positive, second: check_if_near_the_right_number, @@ -622,66 +630,37 @@ def check_the_types( forth: check_if_string_has_right_value, fifth: simple_check, ): - return 'hi' + return "hi" test_cases = [ + ((42, 42, 42, "42", "simple"), (None, None, None, None, None)), + ((43, 43, 43, "42", "simple"), (None, None, "error 3", None, None)), + ((40, 40, 40, "42", "simple"), (None, "error 2", "error 3", None, None)), ( - (42, 42, 42, '42', 'simple',), - ( - None, - None, - None, - None, - None, - ), - ), - ( - (43, 43, 43, '42', 'simple',), - ( - None, - None, - 'error 3', - None, - None, - ), - ), - ( - (40, 40, 40, '42', 'simple',), - ( - None, - 'error 2', - 'error 3', - None, - None, - ), - ), - ( - (-42, -42, -42, '53', 'not_simple',), - ( - 'error 1', - 'error 2', - 'error 3', - 'error 4', - 'error 5', - ), + (-42, -42, -42, "53", "not_simple"), + ("error 1", "error 2", "error 3", "error 4", "error 5"), ), ] for provided_values, expected_results in test_cases: - response = hug.test.get(api, '/check_the_types', **{ - 'first': provided_values[0], - 'second': provided_values[1], - 'third': provided_values[2], - 'forth': provided_values[3], - 'fifth': provided_values[4] - }) - if response.data == 'hi': + response = hug.test.get( + api, + "/check_the_types", + **{ + "first": provided_values[0], + "second": provided_values[1], + "third": provided_values[2], + "forth": provided_values[3], + "fifth": provided_values[4], + } + ) + if response.data == "hi": errors = (None, None, None, None, None) else: errors = [] - for key in ['first', 'second', 'third', 'forth', 'fifth']: - if key in response.data['errors']: - errors.append(response.data['errors'][key]) + for key in ["first", "second", "third", "forth", "fifth"]: + if key in response.data["errors"]: + errors.append(response.data["errors"][key]) else: errors.append(None) errors = tuple(errors) @@ -689,7 +668,7 @@ def check_the_types( def test_extending_types_with_exception_in_function(): - custom_context = dict(context='global', the_only_right_number=42) + custom_context = dict(context="global", the_only_right_number=42) class CustomStrException(Exception): pass @@ -698,14 +677,12 @@ class CustomFunctionException(Exception): pass class CustomNotRegisteredException(ValueError): - def __init__(self): - super().__init__('not registered exception') - + super().__init__("not registered exception") exception_handlers = { - CustomFunctionException: lambda exception: ValueError('function exception'), - CustomStrException: 'string exception', + CustomFunctionException: lambda exception: ValueError("function exception"), + CustomStrException: "string exception", } @hug.context_factory() @@ -725,7 +702,12 @@ def check_simple_exception(value): else: raise CustomFunctionException() - @hug.type(chain=True, extend=hug.types.number, exception_handlers=exception_handlers, accept_context=True) + @hug.type( + chain=True, + extend=hug.types.number, + exception_handlers=exception_handlers, + accept_context=True, + ) def check_context_exception(value, context): if value < 0: raise CustomStrException() @@ -738,7 +720,9 @@ def check_context_exception(value, context): def no_check(value, context): return value - @hug.type(chain=True, extend=no_check, exception_handlers=exception_handlers, accept_context=True) + @hug.type( + chain=True, extend=no_check, exception_handlers=exception_handlers, accept_context=True + ) def check_another_context_exception(value, context): if value < 0: raise CustomStrException() @@ -749,115 +733,92 @@ def check_another_context_exception(value, context): @hug.type(chain=False, exception_handlers=exception_handlers, accept_context=True) def check_simple_no_chain_exception(value, context): - if value == '-1': + if value == "-1": raise CustomStrException() - elif value == '0': + elif value == "0": raise CustomNotRegisteredException() else: raise CustomFunctionException() @hug.type(chain=False, exception_handlers=exception_handlers, accept_context=False) def check_simple_no_chain_no_context_exception(value): - if value == '-1': + if value == "-1": raise CustomStrException() - elif value == '0': + elif value == "0": raise CustomNotRegisteredException() else: raise CustomFunctionException() - - @hug.get('/raise_exception') + @hug.get("/raise_exception") def raise_exception( first: check_simple_exception, second: check_context_exception, third: check_another_context_exception, forth: check_simple_no_chain_exception, - fifth: check_simple_no_chain_no_context_exception + fifth: check_simple_no_chain_no_context_exception, ): return {} - response = hug.test.get(api, '/raise_exception', **{ - 'first': 1, - 'second': 1, - 'third': 1, - 'forth': 1, - 'fifth': 1, - }) - assert response.data['errors'] == { - 'forth': 'function exception', - 'third': 'function exception', - 'fifth': 'function exception', - 'second': 'function exception', - 'first': 'function exception' + response = hug.test.get( + api, "/raise_exception", **{"first": 1, "second": 1, "third": 1, "forth": 1, "fifth": 1} + ) + assert response.data["errors"] == { + "forth": "function exception", + "third": "function exception", + "fifth": "function exception", + "second": "function exception", + "first": "function exception", } - response = hug.test.get(api, '/raise_exception', **{ - 'first': -1, - 'second': -1, - 'third': -1, - 'forth': -1, - 'fifth': -1, - }) - assert response.data['errors'] == { - 'forth': 'string exception', - 'third': 'string exception', - 'fifth': 'string exception', - 'second': 'string exception', - 'first': 'string exception' + response = hug.test.get( + api, + "/raise_exception", + **{"first": -1, "second": -1, "third": -1, "forth": -1, "fifth": -1} + ) + assert response.data["errors"] == { + "forth": "string exception", + "third": "string exception", + "fifth": "string exception", + "second": "string exception", + "first": "string exception", } - response = hug.test.get(api, '/raise_exception', **{ - 'first': 0, - 'second': 0, - 'third': 0, - 'forth': 0, - 'fifth': 0, - }) - assert response.data['errors'] == { - 'second': 'not registered exception', - 'forth': 'not registered exception', - 'third': 'not registered exception', - 'fifth': 'not registered exception', - 'first': 'not registered exception' + response = hug.test.get( + api, "/raise_exception", **{"first": 0, "second": 0, "third": 0, "forth": 0, "fifth": 0} + ) + assert response.data["errors"] == { + "second": "not registered exception", + "forth": "not registered exception", + "third": "not registered exception", + "fifth": "not registered exception", + "first": "not registered exception", } def test_validate_route_args_positive_case(): - class TestSchema(Schema): bar = fields.String() - @hug.get('/hello', args={ - 'foo': fields.Integer(), - 'return': TestSchema() - }) + @hug.get("/hello", args={"foo": fields.Integer(), "return": TestSchema()}) def hello(foo: int) -> dict: - return {'bar': str(foo)} + return {"bar": str(foo)} - response = hug.test.get(api, '/hello', **{ - 'foo': 5 - }) - assert response.data == {'bar': '5'} + response = hug.test.get(api, "/hello", **{"foo": 5}) + assert response.data == {"bar": "5"} def test_validate_route_args_negative_case(): - @hug.get('/hello', raise_on_invalid=True, args={ - 'foo': fields.Integer() - }) + @hug.get("/hello", raise_on_invalid=True, args={"foo": fields.Integer()}) def hello(foo: int): return str(foo) with pytest.raises(ValidationError): - hug.test.get(api, '/hello', **{ - 'foo': 'a' - }) + hug.test.get(api, "/hello", **{"foo": "a"}) class TestSchema(Schema): bar = fields.Integer() - @hug.get('/foo', raise_on_invalid=True, args={ - 'return': TestSchema() - }) + @hug.get("/foo", raise_on_invalid=True, args={"return": TestSchema()}) def foo(): - return {'bar': 'a'} + return {"bar": "a"} with pytest.raises(InvalidTypeData): - hug.test.get(api, '/foo') + hug.test.get(api, "/foo") diff --git a/tests/test_use.py b/tests/test_use.py index c35bd085..f3986654 100644 --- a/tests/test_use.py +++ b/tests/test_use.py @@ -32,91 +32,94 @@ class TestService(object): """Test to ensure the base Service object works as a base Abstract service runner""" - service = use.Service(version=1, timeout=100, raise_on=(500, )) + + service = use.Service(version=1, timeout=100, raise_on=(500,)) def test_init(self): """Test to ensure base service instantiation populates expected attributes""" assert self.service.version == 1 - assert self.service.raise_on == (500, ) + assert self.service.raise_on == (500,) assert self.service.timeout == 100 def test_request(self): """Test to ensure the abstract service request method raises NotImplementedError to show its abstract nature""" with pytest.raises(NotImplementedError): - self.service.request('POST', 'endpoint') + self.service.request("POST", "endpoint") def test_get(self): """Test to ensure the abstract service get method raises NotImplementedError to show its abstract nature""" with pytest.raises(NotImplementedError): - self.service.get('endpoint') + self.service.get("endpoint") def test_post(self): """Test to ensure the abstract service post method raises NotImplementedError to show its abstract nature""" with pytest.raises(NotImplementedError): - self.service.post('endpoint') + self.service.post("endpoint") def test_delete(self): """Test to ensure the abstract service delete method raises NotImplementedError to show its abstract nature""" with pytest.raises(NotImplementedError): - self.service.delete('endpoint') + self.service.delete("endpoint") def test_put(self): """Test to ensure the abstract service put method raises NotImplementedError to show its abstract nature""" with pytest.raises(NotImplementedError): - self.service.put('endpoint') + self.service.put("endpoint") def test_trace(self): """Test to ensure the abstract service trace method raises NotImplementedError to show its abstract nature""" with pytest.raises(NotImplementedError): - self.service.trace('endpoint') + self.service.trace("endpoint") def test_patch(self): """Test to ensure the abstract service patch method raises NotImplementedError to show its abstract nature""" with pytest.raises(NotImplementedError): - self.service.patch('endpoint') + self.service.patch("endpoint") def test_options(self): """Test to ensure the abstract service options method raises NotImplementedError to show its abstract nature""" with pytest.raises(NotImplementedError): - self.service.options('endpoint') + self.service.options("endpoint") def test_head(self): """Test to ensure the abstract service head method raises NotImplementedError to show its abstract nature""" with pytest.raises(NotImplementedError): - self.service.head('endpoint') + self.service.head("endpoint") def test_connect(self): """Test to ensure the abstract service connect method raises NotImplementedError to show its abstract nature""" with pytest.raises(NotImplementedError): - self.service.connect('endpoint') + self.service.connect("endpoint") class TestHTTP(object): """Test to ensure the HTTP Service object enables pulling data from external HTTP services""" - service = use.HTTP('http://www.google.com/', raise_on=(404, 400)) - url_service = use.HTTP('http://www.google.com/', raise_on=(404, 400), json_transport=False) + + service = use.HTTP("http://www.google.com/", raise_on=(404, 400)) + url_service = use.HTTP("http://www.google.com/", raise_on=(404, 400), json_transport=False) def test_init(self): """Test to ensure HTTP service instantiation populates expected attributes""" - assert self.service.endpoint == 'http://www.google.com/' + assert self.service.endpoint == "http://www.google.com/" assert self.service.raise_on == (404, 400) @pytest.mark.extnetwork def test_request(self): """Test so ensure the HTTP service can successfully be used to pull data from an external service""" - response = self.url_service.request('GET', 'search', query='api') + response = self.url_service.request("GET", "search", query="api") assert response assert response.data with pytest.raises(requests.HTTPError): - response = self.service.request('GET', 'search', query='api') + response = self.service.request("GET", "search", query="api") with pytest.raises(requests.HTTPError): - self.url_service.request('GET', 'not_found', query='api') + self.url_service.request("GET", "not_found", query="api") class TestLocal(object): """Test to ensure the Local Service object enables pulling data from internal hug APIs with minimal overhead""" + service = use.Local(__name__) def test_init(self): @@ -125,23 +128,24 @@ def test_init(self): def test_request(self): """Test to ensure requesting data from a local service works as expected""" - assert self.service.get('hello_world').data == 'Hi!' - assert self.service.get('not_there').status_code == 404 - assert self.service.get('validation_error').status_code == 400 + assert self.service.get("hello_world").data == "Hi!" + assert self.service.get("not_there").status_code == 404 + assert self.service.get("validation_error").status_code == 400 self.service.raise_on = (404, 500) with pytest.raises(requests.HTTPError): - assert self.service.get('not_there') + assert self.service.get("not_there") with pytest.raises(requests.HTTPError): - assert self.service.get('exception') + assert self.service.get("exception") class TestSocket(object): """Test to ensure the Socket Service object enables sending/receiving data from arbitrary server/port sockets""" - on_unix = getattr(socket, 'AF_UNIX', False) - tcp_service = use.Socket(connect_to=('www.google.com', 80), proto='tcp', timeout=60) - udp_service = use.Socket(connect_to=('8.8.8.8', 53), proto='udp', timeout=60) + + on_unix = getattr(socket, "AF_UNIX", False) + tcp_service = use.Socket(connect_to=("www.google.com", 80), proto="tcp", timeout=60) + udp_service = use.Socket(connect_to=("8.8.8.8", 53), proto="udp", timeout=60) def test_init(self): """Test to ensure the Socket service instantiation populates the expected attributes""" @@ -149,38 +153,38 @@ def test_init(self): def test_protocols(self): """Test to ensure all supported protocols are present""" - protocols = sorted(['tcp', 'udp', 'unix_stream', 'unix_dgram']) + protocols = sorted(["tcp", "udp", "unix_stream", "unix_dgram"]) if self.on_unix: assert sorted(self.tcp_service.protocols) == protocols else: - protocols.remove('unix_stream') - protocols.remove('unix_dgram') + protocols.remove("unix_stream") + protocols.remove("unix_dgram") assert sorted(self.tcp_service.protocols) == protocols def test_streams(self): if self.on_unix: - assert set(self.tcp_service.streams) == set(('tcp', 'unix_stream', )) + assert set(self.tcp_service.streams) == set(("tcp", "unix_stream")) else: - assert set(self.tcp_service.streams) == set(('tcp', )) + assert set(self.tcp_service.streams) == set(("tcp",)) def test_datagrams(self): if self.on_unix: - assert set(self.tcp_service.datagrams) == set(('udp', 'unix_dgram', )) + assert set(self.tcp_service.datagrams) == set(("udp", "unix_dgram")) else: - assert set(self.tcp_service.datagrams) == set(('udp', )) + assert set(self.tcp_service.datagrams) == set(("udp",)) def test_inet(self): - assert set(self.tcp_service.inet) == set(('tcp', 'udp', )) + assert set(self.tcp_service.inet) == set(("tcp", "udp")) def test_unix(self): if self.on_unix: - assert set(self.tcp_service.unix) == set(('unix_stream', 'unix_dgram', )) + assert set(self.tcp_service.unix) == set(("unix_stream", "unix_dgram")) else: assert set(self.tcp_service.unix) == set() def test_connection(self): - assert self.tcp_service.connection.connect_to == ('www.google.com', 80) - assert self.tcp_service.connection.proto == 'tcp' + assert self.tcp_service.connection.connect_to == ("www.google.com", 80) + assert self.tcp_service.connection.proto == "tcp" assert set(self.tcp_service.connection.sockopts) == set() def test_settimeout(self): @@ -193,28 +197,37 @@ def test_connection_sockopts_unit(self): assert self.tcp_service.connection.sockopts == {(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)} def test_connection_sockopts_batch(self): - self.tcp_service.setsockopt(((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), - (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1))) - assert self.tcp_service.connection.sockopts == {(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), - (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)} + self.tcp_service.setsockopt( + ( + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1), + ) + ) + assert self.tcp_service.connection.sockopts == { + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1), + } @pytest.mark.extnetwork def test_datagram_request(self): """Test to ensure requesting data from a socket service works as expected""" packet = struct.pack("!HHHHHH", 0x0001, 0x0100, 1, 0, 0, 0) - for name in ('www', 'google', 'com'): + for name in ("www", "google", "com"): header = b"!b" header += bytes(str(len(name)), "utf-8") + b"s" - query = struct.pack(header, len(name), name.encode('utf-8')) + query = struct.pack(header, len(name), name.encode("utf-8")) packet = packet + query dns_query = packet + struct.pack("!bHH", 0, 1, 1) - assert len(self.udp_service.request(dns_query.decode("utf-8"), buffer_size=4096).data.read()) > 0 + assert ( + len(self.udp_service.request(dns_query.decode("utf-8"), buffer_size=4096).data.read()) + > 0 + ) @hug.get() def hello_world(): - return 'Hi!' + return "Hi!" @hug.get() diff --git a/tests/test_validate.py b/tests/test_validate.py index bd36889f..1668d64c 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -21,26 +21,30 @@ """ import hug -TEST_SCHEMA = {'first': 'Timothy', 'place': 'Seattle'} +TEST_SCHEMA = {"first": "Timothy", "place": "Seattle"} def test_all(): """Test to ensure hug's all validation function works as expected to combine validators""" - assert not hug.validate.all(hug.validate.contains_one_of('first', 'year'), - hug.validate.contains_one_of('last', 'place'))(TEST_SCHEMA) - assert hug.validate.all(hug.validate.contains_one_of('last', 'year'), - hug.validate.contains_one_of('first', 'place'))(TEST_SCHEMA) + assert not hug.validate.all( + hug.validate.contains_one_of("first", "year"), hug.validate.contains_one_of("last", "place") + )(TEST_SCHEMA) + assert hug.validate.all( + hug.validate.contains_one_of("last", "year"), hug.validate.contains_one_of("first", "place") + )(TEST_SCHEMA) def test_any(): """Test to ensure hug's any validation function works as expected to combine validators""" - assert not hug.validate.any(hug.validate.contains_one_of('last', 'year'), - hug.validate.contains_one_of('first', 'place'))(TEST_SCHEMA) - assert hug.validate.any(hug.validate.contains_one_of('last', 'year'), - hug.validate.contains_one_of('no', 'way'))(TEST_SCHEMA) + assert not hug.validate.any( + hug.validate.contains_one_of("last", "year"), hug.validate.contains_one_of("first", "place") + )(TEST_SCHEMA) + assert hug.validate.any( + hug.validate.contains_one_of("last", "year"), hug.validate.contains_one_of("no", "way") + )(TEST_SCHEMA) def test_contains_one_of(): """Test to ensure hug's contains_one_of validation function works as expected to ensure presence of a field""" - assert hug.validate.contains_one_of('no', 'way')(TEST_SCHEMA) - assert not hug.validate.contains_one_of('last', 'place')(TEST_SCHEMA) + assert hug.validate.contains_one_of("no", "way")(TEST_SCHEMA) + assert not hug.validate.contains_one_of("last", "place")(TEST_SCHEMA) From cad823a4c4212b84ced02beeddf8a2e311ed14bb Mon Sep 17 00:00:00 2001 From: Jason Tyler Date: Mon, 6 May 2019 19:51:30 -0700 Subject: [PATCH 04/17] make flake8 happy --- hug/development_runner.py | 2 +- hug/types.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hug/development_runner.py b/hug/development_runner.py index e1ebba70..6849b391 100644 --- a/hug/development_runner.py +++ b/hug/development_runner.py @@ -77,7 +77,7 @@ def hug( sys.exit(1) sys.argv[1:] = sys.argv[ - (sys.argv.index("-c") if "-c" in sys.argv else sys.argv.index("--command")) + 2 : + (sys.argv.index("-c") if "-c" in sys.argv else sys.argv.index("--command")) + 2: ] api.cli.commands[command]() return diff --git a/hug/types.py b/hug/types.py index 666fb82e..dc6dc06a 100644 --- a/hug/types.py +++ b/hug/types.py @@ -398,7 +398,7 @@ def __call__(self, value): for type_method in self.types: try: return type_method(value) - except: + except BaseException: pass raise ValueError(self.__doc__) From d2228b7ba9d4eee5ffa4edb11051405f9d93b72d Mon Sep 17 00:00:00 2001 From: Jason Tyler Date: Mon, 6 May 2019 20:06:15 -0700 Subject: [PATCH 05/17] make flake8 and black simpatico --- hug/development_runner.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hug/development_runner.py b/hug/development_runner.py index 6849b391..aae06fde 100644 --- a/hug/development_runner.py +++ b/hug/development_runner.py @@ -76,9 +76,10 @@ def hug( print(str(api.cli)) sys.exit(1) - sys.argv[1:] = sys.argv[ - (sys.argv.index("-c") if "-c" in sys.argv else sys.argv.index("--command")) + 2: - ] + use_cli_router = slice( + start=(sys.argv.index("-c") if "-c" in sys.argv else sys.argv.index("--command")) + 2 + ) + sys.argv[1:] = sys.argv[use_cli_router] api.cli.commands[command]() return From f799f2cb8b508bf1bf51eed8bdc59b066df9f74f Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Wed, 8 May 2019 23:42:11 -0700 Subject: [PATCH 06/17] Specify desired change --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02e9b265..2c08708f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ Ideally, within a virtual environment. Changelog ========= +### 2.5.1 hotfix - TBD, +- Fixed issue #784 - POST requests broken on 2.5.0 + ### 2.5.0 - May 4, 2019 - Updated to latest Falcon: 2.0.0 - Added support for Marshmallow 3 From e8498e67b3817a98cbc9ea9a870859eaf781f96b Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 May 2019 00:24:37 -0700 Subject: [PATCH 07/17] Fix issue #784 --- hug/interface.py | 2 +- tests/test_full_request.py | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/test_full_request.py diff --git a/hug/interface.py b/hug/interface.py index 61bb7801..738b06e8 100644 --- a/hug/interface.py +++ b/hug/interface.py @@ -604,7 +604,7 @@ def gather_parameters(self, request, response, context, api_version=None, **inpu input_parameters.update(request.params) if self.parse_body and request.content_length: - body = request.stream + body = request.bounded_stream content_type, content_params = parse_content_type(request.content_type) body_formatter = body and self.inputs.get(content_type, self.api.http.input_format(content_type)) if body_formatter: diff --git a/tests/test_full_request.py b/tests/test_full_request.py new file mode 100644 index 00000000..4071c950 --- /dev/null +++ b/tests/test_full_request.py @@ -0,0 +1,46 @@ +"""tests/test_full_request.py. + +Test cases that rely on a command being ran against a running hug server + +Copyright (C) 2016 Timothy Edmund Crosley + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +""" +import time +from subprocess import Popen + +import requests + +import hug + +TEST_HUG_API = """ +import hug + + +@hug.post("/test", output=hug.output_format.json) +def post(body, response): + print(body) + return {'message': 'ok'} +""" + + +def test_hug_post(tmp_path): + hug_test_file = (tmp_path / "hug_postable.py") + hug_test_file.write_text(TEST_HUG_API) + hug_server = Popen(['hug', '-f', hug_test_file]) + time.sleep(1) + requests.post('http://localhost:8000/test', {'data': 'here'}) + hug_server.kill() From fb7033b1a274fd6181cff12af626cb2d82d9b027 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 May 2019 00:36:45 -0700 Subject: [PATCH 08/17] Fix issue #785 --- CHANGELOG.md | 2 ++ hug/api.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 245e433f..55f8893f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Changelog ========= ### 2.5.1 - TBD - Optimizations and simplification of async support, taking advantadge of Python3.4 deprecation. +- Fix issue #785: Empty query params are not ignored on 2.5.0 +- Added support for modifying falcon API directly on startup ### 2.5.0 - May 4, 2019 - Updated to latest Falcon: 2.0.0 diff --git a/hug/api.py b/hug/api.py index a2396ef9..3dfd405f 100644 --- a/hug/api.py +++ b/hug/api.py @@ -78,11 +78,11 @@ def __init__(self, api): class HTTPInterfaceAPI(InterfaceAPI): """Defines the HTTP interface specific API""" - __slots__ = ( "routes", "versions", "base_url", + "falcon", "_output_format", "_input_format", "versioned", @@ -356,7 +356,8 @@ def version_router( def server(self, default_not_found=True, base_url=None): """Returns a WSGI compatible API server for the given Hug API module""" - falcon_api = falcon.API(middleware=self.middleware) + falcon_api = self.falcon = falcon.API(middleware=self.middleware) + falcon_api.req_options.keep_blank_qs_values = False default_not_found = self.documentation_404() if default_not_found is True else None base_url = self.base_url if base_url is None else base_url From 69593f4d8c4a47d264b967b7f89fa175e505a7a2 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 May 2019 00:38:27 -0700 Subject: [PATCH 09/17] Update contributing guide, and coding standard --- CODING_STANDARD.md | 2 +- CONTRIBUTING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODING_STANDARD.md b/CODING_STANDARD.md index 2a949a9f..dfe6f117 100644 --- a/CODING_STANDARD.md +++ b/CODING_STANDARD.md @@ -2,7 +2,7 @@ Coding Standard ========= Any submission to this project should closely follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) coding guidelines with the exceptions: -1. Lines can be up to 120 characters long. +1. Lines can be up to 100 characters long. 2. Single letter or otherwise nondescript variable names are prohibited. Standards for new hug modules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3da0b3b..a9f1f004 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,7 @@ Account Requirements: Base System Requirements: -- Python3.3+ +- Python3.5+ - Python3-venv (included with most Python3 installations but some Ubuntu systems require that it be installed separately) - bash or a bash compatible shell (should be auto-installed on Linux / Mac) - [autoenv](https://github.com/kennethreitz/autoenv) (optional) From 99bcbad51751eaf5a1570780f906faecd48d0956 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 May 2019 00:42:57 -0700 Subject: [PATCH 10/17] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 245e433f..46f2f9fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Changelog ========= ### 2.5.1 - TBD - Optimizations and simplification of async support, taking advantadge of Python3.4 deprecation. +- Initial `black` formatting of code base, in preperation for CI enforced code formatting ### 2.5.0 - May 4, 2019 - Updated to latest Falcon: 2.0.0 From a30f7696bc0f7280c412146a030e5c87037355ac Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 May 2019 00:46:03 -0700 Subject: [PATCH 11/17] Cast as string --- tests/test_full_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_full_request.py b/tests/test_full_request.py index 4071c950..919876fa 100644 --- a/tests/test_full_request.py +++ b/tests/test_full_request.py @@ -40,7 +40,7 @@ def post(body, response): def test_hug_post(tmp_path): hug_test_file = (tmp_path / "hug_postable.py") hug_test_file.write_text(TEST_HUG_API) - hug_server = Popen(['hug', '-f', hug_test_file]) + hug_server = Popen(['hug', '-f', str(hug_test_file)]) time.sleep(1) requests.post('http://localhost:8000/test', {'data': 'here'}) hug_server.kill() From 3080c44c23adcb3a09fb94343da872b8b26ce9fc Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 May 2019 00:51:50 -0700 Subject: [PATCH 12/17] Remove no longer necessary test ignore logic --- tests/conftest.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 69ad15d8..06955f62 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,11 +2,3 @@ import sys from .fixtures import * - -collect_ignore = [] - -if sys.version_info < (3, 5): - collect_ignore.append("test_async.py") - -if sys.version_info < (3, 4): - collect_ignore.append("test_coroutines.py") From d6e9573605efd5a5fbc02b5749c5ed0b23bd8ea4 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 May 2019 01:19:33 -0700 Subject: [PATCH 13/17] Try to enable hug_server to be able to run and be accessible from within the context of travis ci --- tests/test_full_request.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_full_request.py b/tests/test_full_request.py index 919876fa..e5c677b0 100644 --- a/tests/test_full_request.py +++ b/tests/test_full_request.py @@ -40,7 +40,7 @@ def post(body, response): def test_hug_post(tmp_path): hug_test_file = (tmp_path / "hug_postable.py") hug_test_file.write_text(TEST_HUG_API) - hug_server = Popen(['hug', '-f', str(hug_test_file)]) - time.sleep(1) - requests.post('http://localhost:8000/test', {'data': 'here'}) + hug_server = Popen(['hug', '-f', str(hug_test_file), '-p', '3000']) + time.sleep(5) + requests.post('http://127.0.0.1:3000/test', {'data': 'here'}) hug_server.kill() From d99717e4261c2a45cd1c9ce384025da6249f6028 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 May 2019 01:37:42 -0700 Subject: [PATCH 14/17] Skip running test on PyPy travis --- tests/test_full_request.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_full_request.py b/tests/test_full_request.py index e5c677b0..f7f339a8 100644 --- a/tests/test_full_request.py +++ b/tests/test_full_request.py @@ -19,9 +19,11 @@ OTHER DEALINGS IN THE SOFTWARE. """ +import platform import time from subprocess import Popen +import pytest import requests import hug @@ -37,6 +39,7 @@ def post(body, response): """ +@pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="Can't run hug CLI from travis PyPy") def test_hug_post(tmp_path): hug_test_file = (tmp_path / "hug_postable.py") hug_test_file.write_text(TEST_HUG_API) From 587af4b953cc2589a65fdba523b446f0144d8ed9 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 May 2019 01:46:15 -0700 Subject: [PATCH 15/17] Prepare for a 2.5.1 release --- .bumpversion.cfg | 2 +- .env | 2 +- CHANGELOG.md | 2 +- hug/_version.py | 2 +- setup.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 59dbdba8..44bd7b6e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.5.0 +current_version = 2.5.1 [bumpversion:file:.env] diff --git a/.env b/.env index 518df410..76bc30c5 100644 --- a/.env +++ b/.env @@ -11,7 +11,7 @@ fi export PROJECT_NAME=$OPEN_PROJECT_NAME export PROJECT_DIR="$PWD" -export PROJECT_VERSION="2.5.0" +export PROJECT_VERSION="2.5.1" if [ ! -d "venv" ]; then if ! hash pyvenv 2>/dev/null; then diff --git a/CHANGELOG.md b/CHANGELOG.md index 9997d40d..0f221963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Ideally, within a virtual environment. Changelog ========= -### 2.5.1 hotfix - TBD, +### 2.5.1 hotfix - May 9, 2019 - Fixed issue #784 - POST requests broken on 2.5.0 - Optimizations and simplification of async support, taking advantadge of Python3.4 deprecation. - Fix issue #785: Empty query params are not ignored on 2.5.0 diff --git a/hug/_version.py b/hug/_version.py index 87abd532..4274d84a 100644 --- a/hug/_version.py +++ b/hug/_version.py @@ -21,4 +21,4 @@ """ from __future__ import absolute_import -current = "2.5.0" +current = "2.5.1" diff --git a/setup.py b/setup.py index c7ccafd4..8feea514 100755 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ def list_modules(dirname): setup( name="hug", - version="2.5.0", + version="2.5.1", description="A Python framework that makes developing APIs " "as simple as possible, but no simpler.", long_description=long_description, From be09974c970de4e37c4f6589474d6259a2500330 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 May 2019 01:51:17 -0700 Subject: [PATCH 16/17] Update windows build dependencies --- requirements/build_windows.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/build_windows.txt b/requirements/build_windows.txt index 1fce37da..f8699b8b 100644 --- a/requirements/build_windows.txt +++ b/requirements/build_windows.txt @@ -2,7 +2,7 @@ flake8==3.3.0 isort==4.2.5 marshmallow==2.18.1 -pytest==3.0.7 +pytest==4.3.1 wheel==0.29.0 -pytest-xdist==1.14.0 +pytest-xdist==1.28.0 numpy==1.15.4 From 1f6491b3ce3366c905da421c7ae4c1c1c6b7595c Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Thu, 9 May 2019 01:53:21 -0700 Subject: [PATCH 17/17] Update requirement for Pytest in windows build --- requirements/build_windows.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/build_windows.txt b/requirements/build_windows.txt index f8699b8b..408595fa 100644 --- a/requirements/build_windows.txt +++ b/requirements/build_windows.txt @@ -2,7 +2,7 @@ flake8==3.3.0 isort==4.2.5 marshmallow==2.18.1 -pytest==4.3.1 +pytest==4.4.2 wheel==0.29.0 pytest-xdist==1.28.0 numpy==1.15.4