diff --git a/.coveragerc b/.coveragerc index abfa977..31719d7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,17 +1,20 @@ -[run] -branch = True -omit = - aiohug_swagger/tests/* - aiohug_swagger/*/tests/* - -[report] -skip_covered = True -exclude_lines = - pragma: no cover - def __repr__ - if self.debug: - if settings.DEBUG - raise AssertionError - raise NotImplementedError - if 0: - if __name__ == .__main__.: +[run] +branch = True +omit = + aiohug_swagger/tests/* + aiohug_swagger/*/tests/* + +[report] +skip_covered = True +exclude_lines = + pragma: no cover + def __repr__ + if self.debug: + if settings.DEBUG + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: + +[html] +directory = .reports/coverage diff --git a/.gitignore b/.gitignore index 61284bf..8dbf738 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ __pychache__ /*.egg-info .reports + +.tox/ diff --git a/.travis.yml b/.travis.yml index eae589b..c428a9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,11 @@ language: python python: - '3.7' install: -- pip install -r requirements-test.txt -- pip install coveralls + - pip install -r requirements-test.txt + - pip install coveralls script: -- make ci_test + - make venv + - make ci_test after_success: coveralls deploy: provider: pypi diff --git a/Makefile b/Makefile index 08faa7b..54617ef 100644 --- a/Makefile +++ b/Makefile @@ -4,21 +4,25 @@ PYTHON_VERSION = 3.6 REQUIREMENTS = requirements.txt REQUIREMENTS_TEST = requirements-test.txt VIRTUAL_ENV ?= .venv -PYTHON ?= $(VIRTUAL_ENV)/bin/python +PYTHON = $(VIRTUAL_ENV)/bin/python PIP_CONF = pip.conf PYPI = pypi TEST_SETTINGS = settings_test ci_test: pip install -r $(REQUIREMENTS_TEST) - pytest --cov-report html:.reports/coverage --cov-config .coveragerc --cov $(PROJECT) + make tox -test: venv +test: $(VIRTUAL_ENV)/bin/py.test -test_coverage: venv +test_coverage: $(VIRTUAL_ENV)/bin/py.test --cov-report html:.reports/coverage --cov-config .coveragerc --cov $(PROJECT) +tox: + pip install tox + $(VIRTUAL_ENV)/bin/tox + venv_init: pip install virtualenv if [ ! -d $(VIRTUAL_ENV) ]; then \ @@ -34,5 +38,8 @@ clean_venv: clean_pyc: find . -name \*.pyc -delete -clean: clean_venv clean_pyc +clean_tox: + rm -rf .tox + +clean: clean_venv clean_pyc clean_tox diff --git a/aiohug_swagger/__init__.py b/aiohug_swagger/__init__.py index 6e10a35..6c067ba 100644 --- a/aiohug_swagger/__init__.py +++ b/aiohug_swagger/__init__.py @@ -1,142 +1,13 @@ -import importlib -import logging -from inspect import getfullargspec - -from aiohttp import web -from apispec import APISpec -from apispec.ext.marshmallow import OpenAPIConverter - -from marshmallow import fields, Schema -from marshmallow.schema import SchemaMeta - -from aiohug.arguments import get_default_args -from aiohug.directives import get_available_directives -from .decorators import response, spec, ensure_swagger_attr - -logger = logging.getLogger(__name__) - -converter = OpenAPIConverter("2.1") - -DEFAULT_HOST = "localhost:8080" -DEFAULT_SCHEMES = ["http"] -DEFAULT_VERSION = None -DEFAULT_TITLE = "Swagger Application" -DEFAULT_DEFINITIONS_PATH = None -DEFAULT_TESTING_MODE = False -DEFAULT_USE_DEFAULT_RESPONSE = True -DEFAULT_DESCRIPTION = None -DEFAULT_CONTACT_EMAIL = None - -PARAMETER_IN_PATH = "path" -PARAMETER_IN_QUERY = "query" - - -def get_summary(doc): - if doc is not None: - return doc.split("\n")[0] - - -def where_is_parameter(name, url): - return PARAMETER_IN_PATH if "{%s}" % name in url else PARAMETER_IN_QUERY - - -def get_parameters(url, handler, spec): - defaults = get_default_args(handler._original_handler) - args_spec = getfullargspec(handler._original_handler) - parameters = [] - for name in args_spec.args: - kind = args_spec.annotations.get(name, fields.Field()) - if name in get_available_directives() or name == "request": - continue - - if isinstance(kind, fields.Field): - parameter_place = where_is_parameter(name, url) - kind.metadata = {"location": where_is_parameter(name, url)} - kind.required = name not in defaults - parameter = converter.field2parameter( - kind, name=name, default_in=parameter_place, use_refs=False - ) - if name in defaults: - parameter["default"] = defaults[name] - parameters.append(parameter) - # body - elif name == "body" and ( - isinstance(kind, Schema) or isinstance(kind, SchemaMeta) - ): - if isinstance(kind, Schema): - schema_name = kind.__class__.__name__ - schema = kind - elif isinstance(kind, SchemaMeta): - schema_name = kind.__name__ - schema = kind() - - spec.definition(schema_name, schema=schema) - - ref_definition = "#/definitions/{}".format(schema_name) - ref_schema = {"$ref": ref_definition} - - parameters.append( - {"in": "body", "name": "body", "required": True, "schema": ref_schema} - ) - - return parameters - - -def generate_swagger( - app: web.Application, - title=DEFAULT_TITLE, - version=DEFAULT_VERSION, - host=DEFAULT_HOST, - schemes=DEFAULT_SCHEMES, - definitions_path=DEFAULT_DEFINITIONS_PATH, - **options -): - if host is not None: - options["host"] = host - if schemes is not None: - options["schemes"] = schemes - - spec = APISpec( - title=title, version=version, plugins=("apispec.ext.marshmallow",), **options - ) - if definitions_path is not None: - definitions = importlib.import_module(definitions_path) - - for name, schema in definitions.__dict__.items(): # type: str, Schema - if name.endswith("Schema") and len(name) > len("Schema"): - spec.definition(name, schema=schema) - - resources = app.router._resources - for resource in resources: - url = resource.canonical - name = resource.name - for route in resource._routes: - method = route.method - if method == "HEAD": - continue - - handler = route._handler - - try: - handler_spec = handler.swagger_spec - except AttributeError: - handler_spec = {} - - if "excluded" in handler_spec: - continue - - try: - handler_spec["summary"] = get_summary(handler.__doc__) - handler_spec["description"] = handler.__doc__ - except KeyError: - pass - - parameters = get_parameters(url, handler, spec) - if parameters: - handler_spec["parameters"] = parameters - - handler_spec["operationId"] = name - - spec.add_path(url, operations={method.lower(): handler_spec}) - - return spec.to_dict() +from .swagger import ( + DEFAULT_HOST, + DEFAULT_SCHEMES, + DEFAULT_VERSION, + DEFAULT_OPENAPI_VERSION, + DEFAULT_TITLE, + DEFAULT_TESTING_MODE, + DEFAULT_USE_DEFAULT_RESPONSE, + DEFAULT_DESCRIPTION, + DEFAULT_CONTACT_EMAIL, + generate_spec +) +from .decorators import response, spec diff --git a/aiohug_swagger/decorators.py b/aiohug_swagger/decorators.py index 1781138..9f9b4e9 100644 --- a/aiohug_swagger/decorators.py +++ b/aiohug_swagger/decorators.py @@ -1,7 +1,7 @@ from typing import Optional, Iterable -def ensure_swagger_attr(handler): +def _ensure_swagger_attr(handler): try: handler.swagger_spec except AttributeError: @@ -12,7 +12,7 @@ def response(response_code, schema=None, description=None): """A decorator that adds swagger response""" def decorator(handler): - ensure_swagger_attr(handler) + _ensure_swagger_attr(handler) handler.swagger_spec["responses"][response_code] = {} if schema is not None: handler.swagger_spec["responses"][response_code]["schema"] = schema @@ -33,7 +33,7 @@ def spec( response_codes: Optional[Iterable] = None, ): def decorator(handler): - ensure_swagger_attr(handler) + _ensure_swagger_attr(handler) handler.swagger_spec["private"] = private handler.swagger_spec["exclude"] = exclude handler.swagger_spec["deprecated"] = deprecated diff --git a/aiohug_swagger/handlers.py b/aiohug_swagger/handlers.py index ecb8831..90de918 100644 --- a/aiohug_swagger/handlers.py +++ b/aiohug_swagger/handlers.py @@ -1,24 +1,25 @@ import os + import yaml from aiohttp import web - from aiohug import RouteTableDef -import aiohug_swagger as swagger + +from aiohug_swagger import generate_spec, spec routes = RouteTableDef() -@swagger.spec(exclude=True) +@spec(exclude=True) @routes.get("/swagger.json") async def swagger_json(request): - return swagger.generate_swagger(request.app) + return generate_spec(request.app) -@swagger.spec(exclude=True) +@spec(exclude=True) @routes.get("/swagger.yaml") async def swagger_yaml(request): return web.Response( - text=yaml.dump(swagger.generate_swagger(request.app)), content_type="text/yaml" + text=yaml.dump(generate_spec(request.app)), content_type="text/yaml" ) @@ -30,7 +31,7 @@ async def _render_template(template): return template.read().replace("{{ swagger_url }}", "/swagger.json") -@swagger.spec(exclude=True) +@spec(exclude=True) @routes.get("/swagger.html") async def swagger_html(): return web.Response( @@ -38,10 +39,9 @@ async def swagger_html(): ) -@swagger.spec(exclude=True) +@spec(exclude=True) @routes.get("/redoc.html") async def redoc_html(): - return web.Response( text=await _render_template("redoc.html"), content_type="text/html" ) diff --git a/aiohug_swagger/swagger.py b/aiohug_swagger/swagger.py new file mode 100644 index 0000000..9f835ab --- /dev/null +++ b/aiohug_swagger/swagger.py @@ -0,0 +1,131 @@ +import logging +from inspect import signature, Parameter, isclass +from typing import Optional, Tuple + +from aiohttp import web +from aiohug.directives import get_available_directives +from apispec import APISpec +from apispec.exceptions import DuplicateComponentNameError +from apispec.ext.marshmallow import MarshmallowPlugin, OpenAPIConverter, resolver +from marshmallow import Schema, fields + +logger = logging.getLogger(__name__) + +DEFAULT_HOST = "localhost:8080" +DEFAULT_SCHEMES = ("http",) +DEFAULT_VERSION = None +DEFAULT_OPENAPI_VERSION = "3.0.2" +DEFAULT_TITLE = "Swagger Application" +DEFAULT_TESTING_MODE = False +DEFAULT_USE_DEFAULT_RESPONSE = True +DEFAULT_DESCRIPTION = None +DEFAULT_CONTACT_EMAIL = None + +PARAMETER_IN_PATH = "path" +PARAMETER_IN_QUERY = "query" + + +def get_summary(doc): + if doc is not None: + return doc.split("\n")[0] + + +def where_is_parameter(name, url): + return PARAMETER_IN_PATH if "{%s}" % name in url else PARAMETER_IN_QUERY + + +def get_parameters(url: str, handler, spec, converter): + original_handler = getattr(handler, "_original_handler", None) + if original_handler is None: + # some routes might be created without aiohug decorator, can't do anything about them + return + + handler_signature = signature(original_handler) + + parameters = [] + for name, handler_parameter in handler_signature.parameters.items(): + parameter_kind = handler_parameter.annotation # TODO: support `args` argument to route decorator + + if name in get_available_directives() and name != "body": + continue + + if isinstance(parameter_kind, fields.Field): + parameter_place = where_is_parameter(name, url) + parameter_kind.metadata = {"location": parameter_place} + + has_default = handler_parameter.default != Parameter.empty + + parameter = converter.field2parameter(parameter_kind, name=name, default_in=parameter_place) + + required = True + if has_default: + parameter["default"] = handler_parameter.default + required = False + + parameter["required"] = required + parameters.append(parameter) + elif name == "body": + is_schema_class = isclass(parameter_kind) and issubclass(parameter_kind, Schema) + if not isinstance(parameter_kind, Schema) and not is_schema_class: + continue + + schema = parameter_kind() if is_schema_class else parameter_kind + schema_name = f"{schema.__module__}.{schema.__class__.__name__}" + + try: + spec.components.schema(schema_name, schema=schema) + except DuplicateComponentNameError: # schemas can be reused, no big deal + pass + + ref_definition = "#/components/schemas/{}".format(schema_name) + ref_schema = {"$ref": ref_definition} + + parameters.append( + {"in": "body", "name": "body", "required": True, "schema": ref_schema} + ) + + return parameters + + +def generate_spec( + app: web.Application, + title: str = DEFAULT_TITLE, + version: Optional[str] = DEFAULT_VERSION, + openapi_version: str = DEFAULT_OPENAPI_VERSION, + host: str = DEFAULT_HOST, + schemes: Tuple[str, ...] = DEFAULT_SCHEMES, + **options +): + options["host"] = host + options["schemes"] = schemes + + marshmallow_plugin = MarshmallowPlugin() + + spec = APISpec( + title=title, version=version, openapi_version=openapi_version, plugins=(marshmallow_plugin,), **options + ) + converter = OpenAPIConverter(openapi_version=openapi_version, schema_name_resolver=resolver, spec=spec) + + for route in app.router.routes(): + resource = route.resource + + url = resource.canonical + method = route.method + + handler = route.handler + + handler_spec = getattr(handler, "swagger_spec", {}) + + summary = get_summary(handler.__doc__) + handler_spec["summary"] = summary or url + handler_spec["description"] = handler.__doc__ + + parameters = get_parameters(url, handler, spec, converter) + if parameters: + handler_spec["parameters"] = parameters + + handler_spec["operationId"] = resource.name + + spec.path(url, operations={method.lower(): handler_spec}) + + return spec.to_dict() diff --git a/aiohug_swagger/tests/test_decorators.py b/aiohug_swagger/tests/test_decorators.py index 28c782b..180b5b3 100644 --- a/aiohug_swagger/tests/test_decorators.py +++ b/aiohug_swagger/tests/test_decorators.py @@ -1,7 +1,7 @@ import pytest from marshmallow import Schema, fields -import aiohug_swagger as swagger +from aiohug_swagger.decorators import _ensure_swagger_attr, response, spec class ExampleTestSchema(Schema): @@ -15,7 +15,7 @@ def handler(): with pytest.raises(AttributeError): handler.swagger_spec - swagger.ensure_swagger_attr(handler) + _ensure_swagger_attr(handler) handler.swagger_spec == {"responses": {}} @@ -24,7 +24,7 @@ def test_response(): schema = ExampleTestSchema description = "test" - @swagger.response(code, schema=schema, description=description) + @response(code, schema=schema, description=description) def handler(): pass @@ -36,7 +36,7 @@ def handler(): def test_response_code(): code = 201 - @swagger.response(code) + @response(code) def handler(): pass @@ -47,7 +47,7 @@ def handler(): def test_spec(): attrs = {"private": True, "exclude": True, "deprecated": True, "tags": ["test"]} - @swagger.spec(**attrs) + @spec(**attrs) def handler(): pass @@ -58,7 +58,7 @@ def handler(): def test_spec_response_codes(): codes = [200, 409] - @swagger.spec(response_codes=codes) + @spec(response_codes=codes) def handler(): pass diff --git a/aiohug_swagger/tests/test_handlers.py b/aiohug_swagger/tests/test_handlers.py index 8ec3d9f..fe11c80 100644 --- a/aiohug_swagger/tests/test_handlers.py +++ b/aiohug_swagger/tests/test_handlers.py @@ -1,7 +1,7 @@ import pytest - from aiohttp import web from aiohug import RouteTableDef + from aiohug_swagger.handlers import routes as swagger_handlers @@ -22,21 +22,25 @@ async def ping(): async def test_swagger_json(app, aiohttp_client): client = await aiohttp_client(app) resp = await client.get("/swagger.json") - body = await resp.json() assert resp.status == 200 + await resp.json() assert resp.content_type == "application/json" - # assert body == {} - async def test_swagger_yaml(app, aiohttp_client): client = await aiohttp_client(app) resp = await client.get("/swagger.yaml") assert resp.status == 200 assert resp.content_type == "text/yaml" - text = await resp.text() + await resp.text() + - # assert text == '' +async def test_swagger_html(app, aiohttp_client): + client = await aiohttp_client(app) + resp = await client.get("/swagger.html") + assert resp.status == 200 + assert resp.content_type == "text/html" + await resp.text() async def test_swagger(app, aiohttp_client): @@ -44,5 +48,3 @@ async def test_swagger(app, aiohttp_client): resp = await client.get("/redoc.html") assert resp.status == 200 assert resp.content_type == "text/html" - - # text = await resp.text() diff --git a/aiohug_swagger/tests/test_swagger_generator.py b/aiohug_swagger/tests/test_swagger_generator.py index 8c163e2..71e0324 100644 --- a/aiohug_swagger/tests/test_swagger_generator.py +++ b/aiohug_swagger/tests/test_swagger_generator.py @@ -1,30 +1,219 @@ import pytest +from aiohttp import web +from aiohug import RouteTableDef +from aiohug.directives import directive +from marshmallow import fields, Schema -from aiohug_swagger import get_summary, where_is_parameter +from aiohug_swagger.swagger import ( + generate_spec, + DEFAULT_TITLE, + DEFAULT_VERSION, + DEFAULT_OPENAPI_VERSION, + DEFAULT_HOST, + DEFAULT_SCHEMES, +) + + +@pytest.fixture +def make_app() -> web.Application: + app = web.Application() + + def _make_app(routes=None): + if routes is not None: + app.add_routes(routes) + return app + return _make_app -def test_get_summary(): - doc = """Some string - Hello - """ - assert get_summary(doc) == "Some string" +def _generate_spec(app, **options): + filled_options = {name: option for name, option in options.items() if option is not None} + return generate_spec(app, **filled_options) -def test_get_summary_for_empty_string(): - assert get_summary("") == "" +@pytest.mark.parametrize("title,spec_title", ((None, DEFAULT_TITLE), ("foo", "foo"))) +def test_title(make_app, title, spec_title): + app = make_app() + spec = _generate_spec(app, title=title) + assert spec["info"]["title"] == spec_title -def test_get_summary_for_none(): - assert get_summary(None) is None +@pytest.mark.parametrize("version,spec_version", ((None, DEFAULT_VERSION), ("1.0", "1.0"))) +def test_version(make_app, version, spec_version): + app = make_app() + spec = _generate_spec(app, version=version) + assert spec["info"]["version"] == spec_version @pytest.mark.parametrize( - "name,url,place", - ( - ("page_id", "https://example.com/page/{page_id}?published=False", "path"), - ("page_id", "https://example.com/page/?page_id=5&published=False", "query"), - ), + "openapi_version,spec_openapi_version", ((None, DEFAULT_OPENAPI_VERSION), ("2.1", "2.1"), ("3.1", "3.1")) ) -def test_where_is_parameter(name, url, place): - assert where_is_parameter(name, url) == place +def test_openapi_version(make_app, openapi_version, spec_openapi_version): + app = make_app() + spec = _generate_spec(app, openapi_version=openapi_version) + if openapi_version and openapi_version[0] == "2": + assert spec["swagger"] == spec_openapi_version + else: + assert spec["openapi"] == spec_openapi_version + + +@pytest.mark.parametrize("host,spec_host", ((None, DEFAULT_HOST), ("foo", "foo"))) +def test_host(make_app, host, spec_host): + app = make_app() + spec = _generate_spec(app, host=host) + assert spec["host"] == spec_host + + +@pytest.mark.parametrize("schemes,spec_schemes", ((None, DEFAULT_SCHEMES), (("http", "https"), ("http", "https")))) +def test_schemes(make_app, schemes, spec_schemes): + app = make_app() + spec = _generate_spec(app, schemes=schemes) + assert spec["schemes"] == spec_schemes + + +def test_with_request_parameter(make_app): + routes = RouteTableDef() + + @routes.get("/") + async def with_request_parameter(request): + return "foo" + + app = make_app(routes) + spec = generate_spec(app) + assert "parameters" not in spec["paths"]["/"]["get"] + + +def test_aiohttp_routes(make_app): + aiohttp_routes = web.RouteTableDef() + + @aiohttp_routes.get("/") + async def aiohttp_handler(): + return "foo" + + app = make_app(aiohttp_routes) + spec = generate_spec(app) + assert "parameters" not in spec["paths"]["/"]["get"] + + +def test_custom_directive(make_app): + routes = RouteTableDef() + + @directive + def my_directive(request): + return request + + @routes.get("/") + async def aiohttp_handler(my_directive): + return "foo" + + app = make_app(routes) + spec = generate_spec(app) + assert "parameters" not in spec["paths"]["/"]["get"] + + +def test_wrong_body_type(make_app): + routes = RouteTableDef() + + class A: + pass + + @routes.get("/") + async def with_class(body: A): + return "foo" + + @routes.get("/foo") + async def with_instance(body: A()): + return "foo" + + app = make_app(routes) + spec = generate_spec(app) + assert "parameters" not in spec["paths"]["/"]["get"] + assert "parameters" not in spec["paths"]["/foo"]["get"] + + +def test_with_validated_parameter(make_app): + routes = RouteTableDef() + + @routes.get("/") + async def with_validated_int_parameter(foo: fields.Integer()): + return "foo" + + @routes.get("/bar") + async def with_validated_string_parameter(bar: fields.String()): + return "bar" + + app = make_app(routes) + spec = generate_spec(app) + + foo_parameter = spec["paths"]["/"]["get"]["parameters"][0] + + assert foo_parameter["in"] == "query" + assert foo_parameter["name"] == "foo" + assert foo_parameter["required"] + assert foo_parameter["schema"] == {"format": "int32", "type": "integer"} + + bar_parameter = spec["paths"]["/bar"]["get"]["parameters"][0] + + assert bar_parameter["in"] == "query" + assert bar_parameter["name"] == "bar" + assert bar_parameter["schema"] == {"type": "string"} + + +def test_with_default_parameter(make_app): + routes = RouteTableDef() + + @routes.get("/") + async def with_default_parameter(foo: fields.Integer() = 5): + return "foo" + + app = make_app(routes) + spec = generate_spec(app) + + parameter = spec["paths"]["/"]["get"]["parameters"][0] + + assert parameter["in"] == "query" + assert parameter["name"] == "foo" + assert not parameter["required"] + assert parameter["schema"] == {"format": "int32", "type": "integer"} + + +def test_with_body_schema(make_app): + routes = RouteTableDef() + + class BodySchema(Schema): + a = fields.Integer(required=True) + b = fields.String(required=True) + + @routes.post("/foo") + async def with_body_schema_class(body: BodySchema): + return "" + + @routes.post("/bar") + async def with_body_schema_instance(body: BodySchema()): + return "" + + app = make_app(routes) + spec = generate_spec(app) + + schema_name = f"{with_body_schema_class._original_handler.__module__}.BodySchema" + + foo_parameter = spec["paths"]["/foo"]["post"]["parameters"][0] + assert foo_parameter["in"] == "body" + assert foo_parameter["name"] == "body" + assert foo_parameter["required"] + assert foo_parameter["schema"] == {"$ref": f"#/components/schemas/{schema_name}"} + + bar_parameter = spec["paths"]["/bar"]["post"]["parameters"][0] + + assert bar_parameter["in"] == "body" + assert bar_parameter["name"] == "body" + assert bar_parameter["required"] + assert bar_parameter["schema"] == {"$ref": f"#/components/schemas/{schema_name}"} + + schema = spec["components"]["schemas"][schema_name] + + assert schema == { + "properties": {"a": {"format": "int32", "type": "integer"}, "b": {"type": "string"}}, + "required": ["a", "b"], + "type": "object", + } diff --git a/aiohug_swagger/tests/test_swagger_utils.py b/aiohug_swagger/tests/test_swagger_utils.py new file mode 100644 index 0000000..528110c --- /dev/null +++ b/aiohug_swagger/tests/test_swagger_utils.py @@ -0,0 +1,30 @@ +import pytest + +from aiohug_swagger.swagger import get_summary, where_is_parameter + + +def test_get_summary(): + doc = """Some string + + Hello + """ + assert get_summary(doc) == "Some string" + + +def test_get_summary_for_empty_string(): + assert get_summary("") == "" + + +def test_get_summary_for_none(): + assert get_summary(None) is None + + +@pytest.mark.parametrize( + "name,url,place", + ( + ("page_id", "https://example.com/page/{page_id}?published=False", "path"), + ("page_id", "https://example.com/page/?page_id=5&published=False", "query"), + ), +) +def test_where_is_parameter(name, url, place): + assert where_is_parameter(name, url) == place \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a82fafa..eda432e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ aiohug>=0.6 pyyaml -apispec==0.39.0 +apispec==2.0.0 marshmallow==3.0.0rc4 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..c8706de --- /dev/null +++ b/setup.cfg @@ -0,0 +1,43 @@ +[metadata] +name = aiohug_swagger +version = 1.0b1 +url = https://stash.wargaming.net/projects/WOWSP/repos/hug_extensions +author = nonamenix +author_email = nonamenix@gmail.com +description = Generate swagger documentation for web services developed with aiohug +long_description = file: README.md +license = BSD 3-Clause License +classifiers = + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + +[options.extras_require] +marshmallow_3 = marshmallow==3.0.0rc7 + +[options] +packages = find: +include_package_data = True + +[options.packages.find] +exclude = + tests + +[options.package_data] +* = *.txt, *.md + + +[tool:pytest] +env = + DEFAULTSETTINGS_MODULE = settings_test + TESTS_ON_AIR = yes +python_files = tests.py test_*.py +norecursedirs = .venv* +pep8maxlinelength = 120 + +[tool:coverage:run] +omit = + */tests/* + +[tool:coverage:report] +exclude_lines = + pragma: no cover \ No newline at end of file diff --git a/setup.py b/setup.py index 97ba628..c989462 100644 --- a/setup.py +++ b/setup.py @@ -1,59 +1,24 @@ -from __future__ import unicode_literals -from setuptools import setup, find_packages import os +from configparser import ConfigParser -try: - from configparser import ConfigParser -except ImportError: - from ConfigParser import ConfigParser +from setuptools import setup PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) -PROJECT_INI = os.path.join(PROJECT_DIR, "project.ini") -config = ConfigParser() -config.read(PROJECT_INI) - - -def get_config(opt): - return config.get("project", opt) - - -NAME = get_config("name") -DESCRIPTION = get_config("description") -URL = get_config("url") -AUTHOR = "nonamenix" -AUTHOR_EMAIL = "nonamenix@gmail.com" -README = "README.rst" -README_TXT = "README.txt" -LONG_DESCRIPTION = open(os.path.join(PROJECT_DIR, README)).read() - -REQUIREMENTS_FILE = "requirements.txt" +REQUIREMENTS_FILE = 'requirements.txt' REQUIREMENTS = open(os.path.join(PROJECT_DIR, REQUIREMENTS_FILE)).readlines() -VERSION = get_config("version") -DEV_VERSION = os.environ.get("DEV_VERSION") +DEV_VERSION = os.environ.get('DEV_VERSION') if DEV_VERSION: - VERSION = "{}.dev{}".format(VERSION, DEV_VERSION) - config.set("project", "version", VERSION) - with open(PROJECT_INI, "w") as f: + SETUP_CFG = os.path.join(PROJECT_DIR, 'setup.cfg') + config = ConfigParser() + config.read(SETUP_CFG) + VERSION = config['metadata']['version'] + config['metadata']['version'] = '{}.dev{}'.format(VERSION, DEV_VERSION) + with open(SETUP_CFG, 'w') as f: config.write(f) -# create a README.txt file from .md -with open(README_TXT, "wb") as f: - f.write(LONG_DESCRIPTION.encode()) setup( - name=NAME, - version=VERSION, - description=DESCRIPTION, - long_description=LONG_DESCRIPTION, - author=AUTHOR, - author_email=AUTHOR_EMAIL, - url=URL, - include_package_data=True, - packages=find_packages(), install_requires=REQUIREMENTS, ) - -# delete README.txt -os.remove(README_TXT) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..befe851 --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist=marshmallow-{2,3} + +[testenv] +deps= + -r requirements-dev.txt + marshmallow-2: marshmallow==2.19.5 + marshmallow-3: marshmallow==3.0.0rc7 +whitelist_externals=make +commands=make test_coverage \ No newline at end of file