diff --git a/.checkignore b/.checkignore new file mode 100644 index 000000000..03bce2277 --- /dev/null +++ b/.checkignore @@ -0,0 +1,2 @@ +tests +docs diff --git a/.travis.yml b/.travis.yml index cbd5ee1f0..155ba703d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: python python: + - "pypy" + - "2.7" - "3.4" install: - pip install -e . diff --git a/connexion/api.py b/connexion/api.py index ed88b2398..21899a209 100644 --- a/connexion/api.py +++ b/connexion/api.py @@ -32,8 +32,13 @@ class Api: Single API that corresponds to a flask blueprint """ - def __init__(self, swagger_yaml_path: pathlib.Path, base_url: str=None, arguments: dict=None, - swagger_ui: bool=None): + def __init__(self, swagger_yaml_path, base_url=None, arguments=None, swagger_ui=None): + """ + :type swagger_yaml_path: pathlib.Path + :type base_url: str | None + :type arguments: dict | None + :type swagger_ui: bool + """ self.swagger_yaml_path = pathlib.Path(swagger_yaml_path) logger.debug('Loading specification: %s', swagger_yaml_path, extra={'swagger_yaml': swagger_yaml_path, 'base_url': base_url, @@ -74,7 +79,7 @@ def __init__(self, swagger_yaml_path: pathlib.Path, base_url: str=None, argument self.add_swagger_ui() self.add_paths() - def add_operation(self, method: str, path: str, swagger_operation: dict): + def add_operation(self, method, path, swagger_operation): """ Adds one operation to the api. @@ -86,6 +91,10 @@ def add_operation(self, method: str, path: str, swagger_operation: dict): A friendly name for the operation. The id MUST be unique among all operations described in the API. Tools and libraries MAY use the operation id to uniquely identify an operation. + + :type method: str + :type path: str + :type swagger_operation: dict """ operation = Operation(method=method, path=path, operation=swagger_operation, app_produces=self.produces, app_security=self.security, @@ -95,9 +104,11 @@ def add_operation(self, method: str, path: str, swagger_operation: dict): self.blueprint.add_url_rule(path, operation.endpoint_name, operation.function, methods=[method]) - def add_paths(self, paths: list=None): + def add_paths(self, paths=None): """ Adds the paths defined in the specification as endpoints + + :type paths: list """ paths = paths or self.specification.get('paths', dict()) for path, methods in paths.items(): @@ -128,7 +139,11 @@ def add_swagger_ui(self): index_endpoint_name = "{name}_swagger_ui_index".format(name=self.blueprint.name) self.blueprint.add_url_rule('/ui/', index_endpoint_name, self.swagger_ui_index) - def create_blueprint(self, base_url: str=None) -> flask.Blueprint: + def create_blueprint(self, base_url=None): + """ + :type base_url: str | None + :rtype: flask.Blueprint + """ base_url = base_url or self.base_url logger.debug('Creating API blueprint: %s', base_url) endpoint = utils.flaskify_endpoint(base_url) @@ -139,5 +154,8 @@ def swagger_ui_index(self): return flask.render_template('index.html', api_url=self.base_url) @staticmethod - def swagger_ui_static(filename: str): + def swagger_ui_static(filename): + """ + :type filename: str + """ return flask.send_from_directory(str(SWAGGER_UI_PATH), filename) diff --git a/connexion/app.py b/connexion/app.py index 6050eaf4e..763b845c0 100644 --- a/connexion/app.py +++ b/connexion/app.py @@ -11,10 +11,8 @@ language governing permissions and limitations under the License. """ - import logging import pathlib -import types import flask import tornado.wsgi @@ -29,17 +27,23 @@ class App: - - def __init__(self, import_name: str, port: int=5000, specification_dir: pathlib.Path='', server: str=None, - arguments: dict=None, debug: bool=False, swagger_ui: bool=True): + def __init__(self, import_name, port=5000, specification_dir='', server=None, arguments=None, debug=False, + swagger_ui=True): """ :param import_name: the name of the application package + :type import_name: str :param port: port to listen to + :type port: int :param specification_dir: directory where to look for specifications + :type specification_dir: pathlib.Path | str :param server: which wsgi server to use + :type server: str | None :param arguments: arguments to replace on the specification + :type arguments: dict | None :param debug: include debugging information + :type debug: bool :param swagger_ui: whether to include swagger ui or not + :type swagger_ui: bool """ self.app = flask.Flask(import_name) @@ -65,12 +69,25 @@ def __init__(self, import_name: str, port: int=5000, specification_dir: pathlib. self.arguments = arguments or {} self.swagger_ui = swagger_ui - def add_api(self, swagger_file: pathlib.Path, base_path: str=None, arguments: dict=None, swagger_ui: bool=None): + @staticmethod + def common_error_handler(e): + """ + :type e: Exception + """ + if not isinstance(e, werkzeug.exceptions.HTTPException): + e = werkzeug.exceptions.InternalServerError() + return problem(title=e.name, detail=e.description, status=e.code) + + def add_api(self, swagger_file, base_path=None, arguments=None, swagger_ui=None): """ :param swagger_file: swagger file with the specification + :type swagger_file: pathlib.Path :param base_path: base path where to add this api + :type base_path: str | None :param arguments: api version specific arguments to replace on the specification + :type arguments: dict | None :param swagger_ui: whether to include swagger ui or not + :type swagger_ui: bool """ swagger_ui = swagger_ui if swagger_ui is not None else self.swagger_ui logger.debug('Adding API: %s', swagger_file) @@ -81,16 +98,80 @@ def add_api(self, swagger_file: pathlib.Path, base_path: str=None, arguments: di api = connexion.api.Api(yaml_path, base_path, arguments, swagger_ui) self.app.register_blueprint(api.blueprint) - def add_error_handler(self, error_code: int, function: types.FunctionType): + def add_error_handler(self, error_code, function): + """ + + :type error_code: int + :type function: types.FunctionType + """ self.app.error_handler_spec[None][error_code] = function - @staticmethod - def common_error_handler(e: werkzeug.exceptions.HTTPException): - if not isinstance(e, werkzeug.exceptions.HTTPException): - e = werkzeug.exceptions.InternalServerError() - return problem(title=e.name, detail=e.description, status=e.code) + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """ + Connects a URL rule. Works exactly like the `route` decorator. If a view_func is provided it will be + registered with the endpoint. + + Basically this example:: + + @app.route('/') + def index(): + pass + + Is equivalent to the following:: + + def index(): + pass + app.add_url_rule('/', 'index', index) + + If the view_func is not provided you will need to connect the endpoint to a view function like so:: + + app.view_functions['index'] = index + + Internally`route` invokes `add_url_rule` so if you want to customize the behavior via subclassing you only need + to change this method. + + :param rule: the URL rule as string + :type rule: str + :param endpoint: the endpoint for the registered URL rule. Flask itself assumes the name of the view function as + endpoint + :type endpoint: str + :param view_func: the function to call when serving a request to the provided endpoint + :type view_func: types.FunctionType + :param options: the options to be forwarded to the underlying `werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods is a list of methods this rule should be + limited to (`GET`, `POST` etc.). By default a rule just listens for `GET` (and implicitly + `HEAD`). + """ + log_details = {'endpoint': endpoint, 'view_func': view_func.__name__} + log_details.update(options) + logger.debug('Adding %s', rule, extra=log_details) + self.app.add_url_rule(rule, endpoint, view_func, **options) + + def route(self, rule, **options): + """ + A decorator that is used to register a view function for a + given URL rule. This does the same thing as `add_url_rule` + but is intended for decorator usage:: + + @app.route('/') + def index(): + return 'Hello World' + + :param rule: the URL rule as string + :type rule: str + :param endpoint: the endpoint for the registered URL rule. Flask + itself assumes the name of the view function as + endpoint + :param options: the options to be forwarded to the underlying `werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods is a list of methods this rule should be + limited to (`GET`, `POST` etc.). By default a rule just listens for `GET` (and implicitly + `HEAD`). + """ + logger.debug('Adding %s with decorator', rule, extra=options) + return self.app.route(rule, **options) - def run(self): + def run(self): # pragma: no cover + # this functions is not covered in unit tests because we would effectively testing the mocks logger.debug('Starting {} HTTP server..'.format(self.server), extra=vars(self)) if self.server == 'flask': self.app.run('0.0.0.0', port=self.port, debug=self.debug) diff --git a/connexion/decorators/produces.py b/connexion/decorators/produces.py index c61ad69ea..47656201e 100644 --- a/connexion/decorators/produces.py +++ b/connexion/decorators/produces.py @@ -16,18 +16,23 @@ import functools import json import logging -import types - logger = logging.getLogger('connexion.decorators.produces') class BaseSerializer: def __init__(self, mimetype='text/plain'): + """ + :type mimetype: str + """ self.mimetype = mimetype @staticmethod - def get_data_status_code(data) -> ('Any', int): + def get_data_status_code(data): + """ + :type data: flask.Response | (object, int) | object + :rtype: (object, int) + """ url = flask.request.url logger.debug('Getting data and status code', extra={'data': data, 'data_type': type(data), 'url': url}) if isinstance(data, flask.Response): @@ -42,15 +47,27 @@ def get_data_status_code(data) -> ('Any', int): 'url': url}) return data, status_code - def __call__(self, function: types.FunctionType) -> types.FunctionType: + def __call__(self, function): + """ + :type function: types.FunctionType + :rtype: types.FunctionType + """ return function - def __repr__(self) -> str: + def __repr__(self): + """ + :rtype: str + """ return ''.format(self.mimetype) class Produces(BaseSerializer): - def __call__(self, function: types.FunctionType) -> types.FunctionType: + def __call__(self, function): + """ + :type function: types.FunctionType + :rtype: types.FunctionType + """ + @functools.wraps(function) def wrapper(*args, **kwargs): url = flask.request.url @@ -65,12 +82,20 @@ def wrapper(*args, **kwargs): return wrapper - def __repr__(self) -> str: + def __repr__(self): + """ + :rtype: str + """ return ''.format(self.mimetype) class Jsonifier(BaseSerializer): - def __call__(self, function: types.FunctionType) -> types.FunctionType: + def __call__(self, function): + """ + :type function: types.FunctionType + :rtype: types.FunctionType + """ + @functools.wraps(function) def wrapper(*args, **kwargs): url = flask.request.url @@ -89,5 +114,8 @@ def wrapper(*args, **kwargs): return wrapper - def __repr__(self) -> str: + def __repr__(self): + """ + :rtype: str + """ return ''.format(self.mimetype) diff --git a/connexion/decorators/security.py b/connexion/decorators/security.py index f469c96ee..b89397491 100644 --- a/connexion/decorators/security.py +++ b/connexion/decorators/security.py @@ -17,7 +17,6 @@ import functools import logging import requests -import types from connexion.problem import problem @@ -25,13 +24,24 @@ logger = logging.getLogger('connexion.api.security') -def security_passthrough(function: types.FunctionType) -> types.FunctionType: +def security_passthrough(function): + """ + :type function: types.FunctionType + :rtype: types.FunctionType + """ return function -def verify_oauth(token_info_url: str, allowed_scopes: set, function: types.FunctionType) -> types.FunctionType: +def verify_oauth(token_info_url, allowed_scopes, function): """ Decorator to verify oauth + + :param token_info_url: Url to get information about the token + :type token_info_url: str + :param allowed_scopes: Set with scopes that are allowed to access the endpoint + :type allowed_scopes: set + :type function: types.FunctionType + :rtype: types.FunctionType """ @functools.wraps(function) diff --git a/connexion/decorators/validation.py b/connexion/decorators/validation.py index f690cfb81..3ea2037f6 100644 --- a/connexion/decorators/validation.py +++ b/connexion/decorators/validation.py @@ -16,35 +16,31 @@ import logging import numbers import re -import types +import six +import strict_rfc3339 -from connexion.utils import parse_datetime from connexion.problem import problem -logger = logging.getLogger('connexion.decorators.parameters') +logger = logging.getLogger('connexion.decorators.validation') # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#data-types TYPE_MAP = {'integer': int, 'number': numbers.Number, - 'string': str, + 'string': six.string_types[0], 'boolean': bool, 'array': list, 'object': dict} # map of swagger types to python types - -FORMAT_MAP = {('string', 'date-time'): parse_datetime} +FORMAT_MAP = {('string', 'date-time'): strict_rfc3339.validate_rfc3339} def validate_format(schema, data): schema_type = schema.get('type') schema_format = schema.get('format') func = FORMAT_MAP.get((schema_type, schema_format)) - if func: - try: - func(data) - except: - return "Invalid value, expected {} in '{}' format".format(schema_type, schema_format) + if func and not func(data): + return "Invalid value, expected {} in '{}' format".format(schema_type, schema_format) def validate_pattern(schema, data): @@ -73,7 +69,12 @@ class RequestBodyValidator: def __init__(self, schema): self.schema = schema - def __call__(self, function: types.FunctionType) -> types.FunctionType: + def __call__(self, function): + """ + :type function: types.FunctionType + :rtype: types.FunctionType + """ + @functools.wraps(function) def wrapper(*args, **kwargs): data = flask.request.json @@ -88,7 +89,11 @@ def wrapper(*args, **kwargs): return wrapper - def validate_schema(self, data, schema) -> flask.Response: + def validate_schema(self, data, schema): + """ + :type schema: dict + :rtype: flask.Response | None + """ schema_type = schema.get('type') log_extra = {'url': flask.request.url, 'schema_type': schema_type} diff --git a/connexion/exceptions.py b/connexion/exceptions.py index c0b065018..a9fe4e44b 100644 --- a/connexion/exceptions.py +++ b/connexion/exceptions.py @@ -13,11 +13,15 @@ class ConnexionException(BaseException): - ... + pass class InvalidSpecification(ConnexionException): - def __init__(self, reason: str='Unknown Reason'): + def __init__(self, reason='Unknown Reason'): + """ + :param reason: Reason why the specification is invalid + :type reason: str + """ self.reason = reason def __str__(self): diff --git a/connexion/operation.py b/connexion/operation.py index 1e7545a81..06c959370 100644 --- a/connexion/operation.py +++ b/connexion/operation.py @@ -13,7 +13,6 @@ import functools import logging -import types from connexion.decorators.produces import BaseSerializer, Produces, Jsonifier from connexion.decorators.security import security_passthrough, verify_oauth @@ -29,8 +28,7 @@ class Operation: A single API operation on a path. """ - def __init__(self, method: str, path: str, operation: dict, - app_produces: list, app_security: list, security_definitions: dict, definitions: dict): + def __init__(self, method, path, operation, app_produces, app_security, security_definitions, definitions): """ This class uses the OperationID identify the module and function that will handle the operation @@ -42,8 +40,21 @@ def __init__(self, method: str, path: str, operation: dict, Tools and libraries MAY use the operation id to uniquely identify an operation. :param method: HTTP method + :type method: str :param path: + :type path: str :param operation: swagger operation object + :type operation: dict + :param app_produces: list of content types the application can return by default + :type app_produces: list + :param app_security: list of security rules the application uses by default + :type app_security: list + :param security_definitions: `Security Definitions Object + `_ + :type security_definitions: dict + :param definitions: `Definitions Object + `_ + :type definitions: dict """ self.method = method @@ -62,7 +73,7 @@ def __init__(self, method: str, path: str, operation: dict, self.__undecorated_function = get_function_from_name(self.operation_id) @property - def body_schema(self) -> dict: + def body_schema(self): """ `About operation parameters `_ @@ -72,11 +83,13 @@ def body_schema(self) -> dict: parameters. A unique parameter is defined by a combination of a name and location. The list can use the Reference Object to link to parameters that are defined at the Swagger Object's parameters. **There can be one "body" parameter at most.** + + :rtype: dict """ body_parameters = [parameter for parameter in self.parameters if parameter['in'] == 'body'] if len(body_parameters) > 1: raise InvalidSpecification( - "{method} {path} There can be one 'body' parameter at most".format_map(vars(self))) + "{method} {path} There can be one 'body' parameter at most".format(**vars(self))) body_parameters = body_parameters[0] if body_parameters else {} schema = body_parameters.get('schema') # type: dict @@ -87,7 +100,7 @@ def body_schema(self) -> dict: if reference: if not reference.startswith('#/definitions/'): raise InvalidSpecification( - "{method} {path} '$ref' needs to to point to definitions".format_map(vars(self))) + "{method} {path} '$ref' needs to to point to definitions".format(**vars(self))) definition_name = reference[14:] try: schema.update(self.definitions[definition_name]) @@ -98,7 +111,12 @@ def body_schema(self) -> dict: return schema @property - def function(self) -> types.FunctionType: + def function(self): + """ + Operation function with decorators + + :rtype: types.FunctionType + """ produces_decorator = self.__content_type_decorator logger.debug('... Adding produces decorator (%r)', produces_decorator, extra=vars(self)) function = produces_decorator(self.__undecorated_function) @@ -114,7 +132,7 @@ def function(self) -> types.FunctionType: return function @property - def __content_type_decorator(self) -> types.FunctionType: + def __content_type_decorator(self): """ Get produces decorator. @@ -126,6 +144,8 @@ def __content_type_decorator(self) -> types.FunctionType: A list of MIME types the operation can produce. This overrides the produces definition at the Swagger Object. An empty value MAY be used to clear the global definition. + + :rtype: types.FunctionType """ logger.debug('... Produces: %s', self.produces, extra=vars(self)) @@ -144,7 +164,7 @@ def __content_type_decorator(self) -> types.FunctionType: return BaseSerializer() @property - def __security_decorator(self) -> types.FunctionType: + def __security_decorator(self): """ Gets the security decorator for operation @@ -164,6 +184,8 @@ def __security_decorator(self) -> types.FunctionType: declared in it which are all required (that is, there is a logical AND between the schemes). The name used for each property **MUST** correspond to a security scheme declared in the Security Definitions. + + :rtype: types.FunctionType """ logger.debug('... Security: %s', self.security, extra=vars(self)) if self.security: @@ -194,6 +216,9 @@ def __security_decorator(self) -> types.FunctionType: return security_passthrough @property - def __validation_decorator(self) -> types.FunctionType: + def __validation_decorator(self): + """ + :rtype: types.FunctionType + """ if self.body_schema: return RequestBodyValidator(self.body_schema) diff --git a/connexion/problem.py b/connexion/problem.py index cb8df96e8..cf9c73afc 100644 --- a/connexion/problem.py +++ b/connexion/problem.py @@ -14,19 +14,25 @@ import json -def problem(status: int, title: str, detail: str, type='about:blank', instance: str=None): +def problem(status, title, detail, type='about:blank', instance=None): """ Returns a `Problem Details `_ error response. :param type: An absolute URI that identifies the problem type. When dereferenced, it SHOULD provide human-readable documentation for the problem type (e.g., using HTML). When this member is not present its value is assumed to be "about:blank". + :type: type: str :param title: A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localisation. + :type title: str :param detail: An human readable explanation specific to this occurrence of the problem. + :type detail: str :param status: The HTTP status code generated by the origin server for this occurrence of the problem. + :type status: int :param instance: An absolute URI that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. + :type instance: str + :type type: str | None :return: Json serialized error response """ problem_response = {'type': type, 'title': title, 'detail': detail, 'status': status, } diff --git a/connexion/utils.py b/connexion/utils.py index 563189b13..b11c23e8c 100644 --- a/connexion/utils.py +++ b/connexion/utils.py @@ -11,16 +11,18 @@ language governing permissions and limitations under the License. """ -import datetime import importlib import re PATH_PARAMETER = re.compile(r'\{([^}]*)\}') -def flaskify_endpoint(identifier: str) -> str: +def flaskify_endpoint(identifier): """ Converts the provided identifier in a valid flask endpoint name + + :type identifier: str + :rtype: str """ return identifier.replace('.', '_') @@ -29,10 +31,13 @@ def convert_path_parameter(match): return '<{}>'.format(match.group(1).replace('-', '_')) -def flaskify_path(swagger_path: str) -> str: +def flaskify_path(swagger_path): """ Convert swagger path templates to flask path templates + :type swagger_path: str + :rtype: str + >>> flaskify_path('/foo-bar/{my-param}') '/foo-bar/' """ @@ -40,15 +45,21 @@ def flaskify_path(swagger_path: str) -> str: return PATH_PARAMETER.sub(convert_path_parameter, swagger_path) -def get_function_from_name(operation_id: str) -> str: - module_name, function_name = operation_id.rsplit('.', maxsplit=1) +def get_function_from_name(operation_id): + """ + :type operation_id: str + """ + module_name, function_name = operation_id.rsplit('.', 1) module = importlib.import_module(module_name) function = getattr(module, function_name) return function -def produces_json(produces: list) -> bool: +def produces_json(produces): """ + :type produces: list + :rtype: bool + >>> produces_json(['application/json']) True >>> produces_json(['application/x.custom+json']) @@ -72,20 +83,3 @@ def produces_json(produces: list) -> bool: # todo handle parameters maintype, subtype = mimetype.split('/') # type: str, str return maintype == 'application' and subtype.endswith('+json') - - -def parse_datetime(s: str): - '''http://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14''' - if '.' in s: - time_secfrac = '.%f' - else: - # missing "time-secfrac" (milliseconds) - time_secfrac = '' - try: - # "Z" for UTC - datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S{}Z'.format(time_secfrac)) - except: - # "+02:00" time zone offset - # remove the ":" first (%z expects "+0200") - x = s[:-3] + s[-2:] - datetime.datetime.strptime(x, '%Y-%m-%dT%H:%M:%S{}%z'.format(time_secfrac)) diff --git a/setup.py b/setup.py index b386ad4f6..12e022c24 100755 --- a/setup.py +++ b/setup.py @@ -1,16 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import platform import sys from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand -version = '0.7.6' +version = '0.8' +py_major_version, py_minor_version, _ = (int(v) for v in platform.python_version_tuple()) +requires = ['flask', 'PyYAML', 'tornado', 'requests', 'six', 'strict-rfc3339'] + +if py_major_version == 2 or (py_major_version == 3 and py_minor_version < 4): + requires.append('pathlib') -class PyTest(TestCommand): +class PyTest(TestCommand): def initialize_options(self): TestCommand.initialize_options(self) self.cov = None @@ -23,6 +29,7 @@ def finalize_options(self): def run_tests(self): import pytest + errno = pytest.main(self.pytest_args) sys.exit(errno) @@ -36,11 +43,12 @@ def run_tests(self): author='Zalando SE', url='https://github.com/zalando/connexion', license='Apache License Version 2.0', - install_requires=['flask', 'PyYAML', 'tornado', 'requests'], + install_requires=requires, tests_require=['pytest-cov', 'pytest'], cmdclass={'test': PyTest}, classifiers=[ 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', diff --git a/tests/fakeapi/hello.py b/tests/fakeapi/hello.py index 876ce0eeb..9c399aacf 100755 --- a/tests/fakeapi/hello.py +++ b/tests/fakeapi/hello.py @@ -3,21 +3,21 @@ from connexion import problem -def post_greeting(name: str) -> dict: +def post_greeting(name): data = {'greeting': 'Hello {name}'.format(name=name)} return data -def get_list(name: str) -> list: +def get_list(name): data = ['hello', name] return data -def get_bye(name: str) -> str: +def get_bye(name): return 'Goodbye {name}'.format(name=name), 200 -def get_bye_secure(name: str) -> str: +def get_bye_secure(name): return 'Goodbye {name} (Secure)'.format(name=name) @@ -40,7 +40,7 @@ def internal_error(): return 42 / 0 -def get_greetings(name: str) -> dict: +def get_greetings(name): """ Used to test custom mimetypes """ @@ -48,7 +48,7 @@ def get_greetings(name: str) -> dict: return data -def multimime() -> str: +def multimime(): return 'Goodbye' diff --git a/tests/test_app.py b/tests/test_app.py index 37d7dc76c..940a8b55c 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -15,7 +15,11 @@ class FakeResponse: - def __init__(self, status_code: int, text: str): + def __init__(self, status_code, text): + """ + :type status_code: int + :type text: ste + """ self.status_code = status_code self.text = text self.ok = status_code == 200 @@ -25,8 +29,12 @@ def json(self): @pytest.fixture -def oauth_requests(monkeypatch: '_pytest.monkeypatch.monkeypatch'): - def fake_get(url: str, params: dict=None): +def oauth_requests(monkeypatch): + def fake_get(url, params=None): + """ + :type url: str + :type params: dict| None + """ params = params or {} if url == "https://ouath.example/token_info": token = params['access_token'] @@ -285,10 +293,35 @@ def test_schema_format(app): app_client = app.app.test_client() headers = {'Content-type': 'application/json'} - wrong_type = app_client.post('/v1.0/test_schema_format', headers=headers, data=json.dumps("xy")) # type: flask.Response + wrong_type = app_client.post('/v1.0/test_schema_format', headers=headers, + data=json.dumps("xy")) # type: flask.Response assert wrong_type.status_code == 400 assert wrong_type.content_type == 'application/problem+json' wrong_type_response = json.loads(wrong_type.data.decode()) # type: dict assert wrong_type_response['title'] == 'Bad Request' assert wrong_type_response['detail'] == "Invalid value, expected string in 'date-time' format" + +def test_single_route(app): + def route1(): + return 'single 1' + + @app.route('/single2', methods=['POST']) + def route2(): + return 'single 2' + + app_client = app.app.test_client() + + app.add_url_rule('/single1', 'single1', route1, methods=['GET']) + + get_single1 = app_client.get('/single1') # type: flask.Response + assert get_single1.data == b'single 1' + + post_single1 = app_client.post('/single1') # type: flask.Response + assert post_single1.status_code == 405 + + post_single2 = app_client.post('/single2') # type: flask.Response + assert post_single2.data == b'single 2' + + get_single2 = app_client.get('/single2') # type: flask.Response + assert get_single2.status_code == 405 diff --git a/tests/test_utils.py b/tests/test_utils.py index 7427a1ba5..41ba5adbe 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -21,9 +21,3 @@ def test_get_function_from_name(): function = utils.get_function_from_name('math.ceil') assert function == math.ceil assert function(2.7) == 3 - - -def test_parse_datetime(): - utils.parse_datetime('2015-05-05T01:01:01.001+02:00') - utils.parse_datetime('2015-05-05T01:01:01Z') - utils.parse_datetime('2015-07-23T18:34:32+02:00')