From de5a9da6338b164a2d41f9be29acbafb0e3c0ec8 Mon Sep 17 00:00:00 2001 From: EricM Date: Tue, 16 Feb 2021 11:59:36 +0100 Subject: [PATCH 01/11] filter by resource --- falcon_apispec/falcon_plugin.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/falcon_apispec/falcon_plugin.py b/falcon_apispec/falcon_plugin.py index 9583a93..9fa5403 100644 --- a/falcon_apispec/falcon_plugin.py +++ b/falcon_apispec/falcon_plugin.py @@ -12,32 +12,30 @@ def __init__(self, app): self._app = app @staticmethod - def _generate_resource_uri_mapping(app): + def _generate_resource_uri_mapping(app, resource): routes_to_check = copy.copy(app._router._roots) mapping = {} for route in routes_to_check: - uri = route.uri_template - resource = route.resource - mapping[resource] = { - "uri": uri, - "methods": {} - } - - if route.method_map: - for method_name, method_handler in route.method_map.items(): - if method_handler.__dict__.get("__module__") == "falcon.responders": - continue - mapping[resource]["methods"][method_name.lower()] = method_handler + if route.resource == resource: + uri = route.uri_template + mapping[uri] = {} + + if route.method_map: + for method_name, method_handler in route.method_map.items(): + if method_handler.__dict__.get("__module__") == "falcon.responders": + continue + mapping[uri][method_name.lower()] = method_handler routes_to_check.extend(route.children) return mapping def path_helper(self, operations, resource, base_path=None, **kwargs): """Path helper that allows passing a Falcon resource instance.""" - resource_uri_mapping = self._generate_resource_uri_mapping(self._app) + resource_uri_mapping = self._generate_resource_uri_mapping(self._app, resource) + print(resource_uri_mapping) - if resource not in resource_uri_mapping: + if not resource_uri_mapping: raise APISpecError("Could not find endpoint for resource {0}".format(resource)) operations.update(yaml_utils.load_operations_from_docstring(resource.__doc__) or {}) From 10bdf425457be97a9cf5285dd5fa022ec5fa7b07 Mon Sep 17 00:00:00 2001 From: EricM Date: Tue, 16 Feb 2021 14:54:32 +0100 Subject: [PATCH 02/11] Filters out suffixes, unless suffix kwarg is provided upon spec.path() --- falcon_apispec/falcon_plugin.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/falcon_apispec/falcon_plugin.py b/falcon_apispec/falcon_plugin.py index 9fa5403..76e274c 100644 --- a/falcon_apispec/falcon_plugin.py +++ b/falcon_apispec/falcon_plugin.py @@ -12,34 +12,41 @@ def __init__(self, app): self._app = app @staticmethod - def _generate_resource_uri_mapping(app, resource): + def _generate_resource_uri_mapping(app, resource, suffix): routes_to_check = copy.copy(app._router._roots) mapping = {} for route in routes_to_check: - if route.resource == resource: - uri = route.uri_template - mapping[uri] = {} - + uri = route.uri_template + if route.resource == resource and ((suffix is not None and uri.endswith(suffix) is True) or suffix is None): if route.method_map: + print(route.method_map) + methods = {} for method_name, method_handler in route.method_map.items(): - if method_handler.__dict__.get("__module__") == "falcon.responders": + if method_handler.__dict__.get("__module__") == "falcon.responders" or \ + (suffix is not None and not method_handler.__name__.lower().endswith(suffix)) or \ + (suffix is None and not method_handler.__name__.lower().endswith(method_name.lower())): continue - mapping[uri][method_name.lower()] = method_handler + methods.update({method_name.lower(): method_handler}) + if methods: + mapping[uri] = methods routes_to_check.extend(route.children) return mapping - def path_helper(self, operations, resource, base_path=None, **kwargs): + def path_helper(self, operations, resource, base_path=None, suffix=None, **kwargs): """Path helper that allows passing a Falcon resource instance.""" - resource_uri_mapping = self._generate_resource_uri_mapping(self._app, resource) - print(resource_uri_mapping) + resource_uri_mapping = self._generate_resource_uri_mapping(self._app, resource, suffix) if not resource_uri_mapping: raise APISpecError("Could not find endpoint for resource {0}".format(resource)) operations.update(yaml_utils.load_operations_from_docstring(resource.__doc__) or {}) - path = resource_uri_mapping[resource]["uri"] + + path = next(iter(resource_uri_mapping)) + if len(resource_uri_mapping.keys()) > 1: + print(resource_uri_mapping) + raise APISpecError("More than one uri found") if base_path is not None: # make sure base_path accept either with or without leading slash @@ -47,7 +54,7 @@ def path_helper(self, operations, resource, base_path=None, **kwargs): base_path = '/' + base_path.strip('/') path = re.sub(base_path, "", path, 1) - methods = resource_uri_mapping[resource]["methods"] + methods = resource_uri_mapping[path] for method_name, method_handler in methods.items(): docstring_yaml = yaml_utils.load_yaml_from_docstring(method_handler.__doc__) From bdbd6aa9e8acff60e0c65c1b1022e70fe1c1dc01 Mon Sep 17 00:00:00 2001 From: EricM Date: Tue, 16 Feb 2021 16:36:44 +0100 Subject: [PATCH 03/11] Improved tox, added tests --- .coveragerc | 22 +++++++ falcon_apispec/falcon_plugin.py | 14 ++--- tests/falcon_test.py | 100 +++++++++++++++++++++++--------- tox.ini | 31 +++++++++- 4 files changed, 133 insertions(+), 34 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..95a8367 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,22 @@ +[paths] +source = + falcon_apispec + */site-packages + +[run] +branch = true +source = + falcon_apispec + tests +parallel = true + +[report] +show_missing = true +precision = 2 +omit = *migrations* +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + # Don't complain if tests don't hit defensive assertion code: + raise NotImplementedError + pass diff --git a/falcon_apispec/falcon_plugin.py b/falcon_apispec/falcon_plugin.py index 76e274c..984f189 100644 --- a/falcon_apispec/falcon_plugin.py +++ b/falcon_apispec/falcon_plugin.py @@ -19,8 +19,8 @@ def _generate_resource_uri_mapping(app, resource, suffix): for route in routes_to_check: uri = route.uri_template if route.resource == resource and ((suffix is not None and uri.endswith(suffix) is True) or suffix is None): + mapping[uri] = {} if route.method_map: - print(route.method_map) methods = {} for method_name, method_handler in route.method_map.items(): if method_handler.__dict__.get("__module__") == "falcon.responders" or \ @@ -43,10 +43,12 @@ def path_helper(self, operations, resource, base_path=None, suffix=None, **kwarg operations.update(yaml_utils.load_operations_from_docstring(resource.__doc__) or {}) - path = next(iter(resource_uri_mapping)) - if len(resource_uri_mapping.keys()) > 1: - print(resource_uri_mapping) - raise APISpecError("More than one uri found") + try: + path = next(uri for uri, methods in resource_uri_mapping.items() if methods) + except StopIteration: + path = next(iter(resource_uri_mapping)) + + methods = resource_uri_mapping[path] if base_path is not None: # make sure base_path accept either with or without leading slash @@ -54,8 +56,6 @@ def path_helper(self, operations, resource, base_path=None, suffix=None, **kwarg base_path = '/' + base_path.strip('/') path = re.sub(base_path, "", path, 1) - methods = resource_uri_mapping[path] - for method_name, method_handler in methods.items(): docstring_yaml = yaml_utils.load_yaml_from_docstring(method_handler.__doc__) operations[method_name] = docstring_yaml or dict() diff --git a/tests/falcon_test.py b/tests/falcon_test.py index e5fadee..ad113ad 100644 --- a/tests/falcon_test.py +++ b/tests/falcon_test.py @@ -6,6 +6,32 @@ from falcon_apispec import FalconPlugin +@pytest.fixture +def suffixed_resource(): + class SuffixedResource: + def on_get_hello(self): + """A greeting endpoint. + --- + description: get a greeting + responses: + 200: + description: said hi + """ + return "dummy_hello" + + def on_get(self): + """An invalid method. + --- + description: get something + responses: + 200: + description: said ??? + """ + return "dummy" + + return SuffixedResource() + + @pytest.fixture() def spec_factory(): def _spec(app): @@ -139,40 +165,62 @@ def on_get(self, req, resp): assert spec._paths["/foo/v1"]["get"] == expected - def test_path_with_suffix(self, app, spec_factory): - class HelloResource: - def on_get_hello(self): - """A greeting endpoint. - --- - description: get a greeting - responses: - 200: - description: said hi - """ - return "dummy" - - def on_get(self): - """An invalid method. - --- - description: this should not pass - responses: - 200: - description: said hi - """ - return "invalid" - + def test_path_with_suffix(self, app, spec_factory, suffixed_resource): expected = { "description": "get a greeting", "responses": {"200": {"description": "said hi"}}, } - hello_resource_with_suffix = HelloResource() - app.add_route("/hi", hello_resource_with_suffix, suffix="hello") + app.add_route("/hello", suffixed_resource, suffix="hello") spec = spec_factory(app) - spec.path(resource=hello_resource_with_suffix) + spec.path(resource=suffixed_resource, suffix="hello") - assert spec._paths["/hi"]["get"] == expected + assert spec._paths["/hello"]["get"] == expected + + def test_path_ignore_suffix(self, app, spec_factory, suffixed_resource): + expected = { + "description": "get something", + "responses": {"200": {"description": "said ???"}}, + } + + app.add_route("/say", suffixed_resource) + + spec = spec_factory(app) + spec.path(resource=suffixed_resource) + + assert spec._paths["/say"]["get"] == expected + + def test_path_suffix_all(self, app, spec_factory, suffixed_resource): + + app.add_route("/say", suffixed_resource) + app.add_route("/say/hello", suffixed_resource, suffix="hello") + + spec = spec_factory(app) + spec.path(resource=suffixed_resource) + spec.path(resource=suffixed_resource, suffix="hello") + + assert spec._paths["/say"]["get"]["description"] == "get something" + assert spec._paths["/say/hello"]["get"]["description"] == "get a greeting" + + def test_path_multiple_routes_same_resource(self, app, spec_factory): + class HelloResource: + """Greeting API. + --- + x-extension: global metadata + """ + + hello_resource = HelloResource() + app.add_route("/hi", hello_resource) + app.add_route("/greet", hello_resource) + + spec = spec_factory(app) + spec.path(resource=hello_resource) + + assert spec._paths["/hi"]["x-extension"] == "global metadata" + with pytest.raises(KeyError): + # Limitation: one route will not be documented!!! + assert spec._paths["/greet"]["x-extension"] == "global metadata" def test_resource_without_endpoint(self, app, spec_factory): class HelloResource: diff --git a/tox.ini b/tox.ini index e6aa563..5687960 100644 --- a/tox.ini +++ b/tox.ini @@ -1,2 +1,31 @@ +[tox] +envlist = + check, + {py35, py37} + +[testenv] +basepython = + py35: {env:TOXPYTHON:python3.5} + py37: {env:TOXPYTHON:python3.7} + {docs,clean,check,report}: {env:TOXPYTHON:python3} +setenv = + PYTHONPATH={toxinidir}/tests + PYTHONUNBUFFERED=yes +passenv = + * +usedevelop = false +deps = + pytest + pytest-cov +commands = + pytest --cov --cov-report=term-missing -vv tests {posargs} + +[testenv:check] +deps = + flake8 +skip_install = true +commands = + flake8 falcon_apispec tests setup.py + [flake8] -max_line_length = 99 +max-line-length = 120 \ No newline at end of file From 81314ebb991cfefa2b220aba1245541c504949f8 Mon Sep 17 00:00:00 2001 From: EricM Date: Tue, 16 Feb 2021 16:43:25 +0100 Subject: [PATCH 04/11] added extra requires, bump2version config --- .bumpversion.cfg | 21 +++++++++++++++++++++ setup.py | 3 +++ 2 files changed, 24 insertions(+) create mode 100644 .bumpversion.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..a086a06 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,21 @@ +[bumpversion] +current_version = 0.4.0 +commit = True +tag = False +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z]+)?(?P\d+)? +serialize = + {major}.{minor}.{patch}{release}{inc} + {major}.{minor}.{patch} + +[bumpversion:part:release] +optional_value = production +first_value = alpha +values = + alpha + rc + production + +[bumpversion:file:falcon_apispec/version.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' + diff --git a/setup.py b/setup.py index 24e15e1..eabad56 100644 --- a/setup.py +++ b/setup.py @@ -56,6 +56,9 @@ def find_version(*file_paths): "apispec>=1.0", "falcon", ], + extras_require={ + 'dev': ['bump2version', 'tox', 'pytest', 'pytest-cov'], + }, packages=find_packages(exclude=["tests", ]), test_suite='tests', From b1c657b454e494538ccba62186ea1a977ac07050 Mon Sep 17 00:00:00 2001 From: EricM Date: Tue, 16 Feb 2021 16:44:09 +0100 Subject: [PATCH 05/11] =?UTF-8?q?Bump=20version:=200.4.0=20=E2=86=92=200.5?= =?UTF-8?q?.0alpha0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 7 +++---- falcon_apispec/version.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a086a06..c5f5d76 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.0 +current_version = 0.5.0alpha0 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(?P[a-z]+)?(?P\d+)? @@ -16,6 +16,5 @@ values = production [bumpversion:file:falcon_apispec/version.py] -search = __version__ = '{current_version}' -replace = __version__ = '{new_version}' - +search = __version__ = "{current_version}" +replace = __version__ = "{new_version}" diff --git a/falcon_apispec/version.py b/falcon_apispec/version.py index 6a9beea..2767f58 100644 --- a/falcon_apispec/version.py +++ b/falcon_apispec/version.py @@ -1 +1 @@ -__version__ = "0.4.0" +__version__ = "0.5.0alpha0" From 4d7dde8de5e5a0455cb3b2835a3675b084856151 Mon Sep 17 00:00:00 2001 From: EricM Date: Tue, 16 Feb 2021 16:56:29 +0100 Subject: [PATCH 06/11] readme --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index 2278b4b..17d09d5 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,46 @@ spec.to_yaml() # tags: [] ``` + + +### Falcon Route Suffix Support + +Since Falcon 2.0, a single resource may contain several responders of the same HTTP type (e.g. 2 GETs) if a suffix is added at route creation. + +falcon-apispec >= 0.5 supports this through multiple APISpec path registration: + +```python +class SuffixedHelloResource: + def on_get_hello(self): + """A greeting endpoint. + --- + description: get a greeting + responses: + 200: + description: said hi + """ + return "dummy_hello" + + def on_get(self): + """Base method. + --- + description: get something + responses: + 200: + description: said ??? + """ + return "dummy" + + +suffixed_resource = SuffixedHelloResource() +app.add_route("/say", suffixed_resource) +app.add_route("/say/hello", suffixed_resource, suffix="hello") + +spec = spec_factory(app) +spec.path(resource=suffixed_resource) # registers on_get +spec.path(resource=suffixed_resource, suffix="hello") # registers on_get_hello +``` + ## Contributing ### Setting Up for Local Development From d02d4a20a29e8eb36972373cfc7c377e737e5598 Mon Sep 17 00:00:00 2001 From: EricM Date: Wed, 17 Feb 2021 09:29:39 +0100 Subject: [PATCH 07/11] useless if --- falcon_apispec/falcon_plugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/falcon_apispec/falcon_plugin.py b/falcon_apispec/falcon_plugin.py index 984f189..4688e93 100644 --- a/falcon_apispec/falcon_plugin.py +++ b/falcon_apispec/falcon_plugin.py @@ -28,8 +28,7 @@ def _generate_resource_uri_mapping(app, resource, suffix): (suffix is None and not method_handler.__name__.lower().endswith(method_name.lower())): continue methods.update({method_name.lower(): method_handler}) - if methods: - mapping[uri] = methods + mapping[uri] = methods routes_to_check.extend(route.children) return mapping From f146d21efdad7eeb859fcf7f0a5c5f7d4177682d Mon Sep 17 00:00:00 2001 From: EricM Date: Wed, 17 Feb 2021 14:13:06 +0100 Subject: [PATCH 08/11] Comments in code --- falcon_apispec/falcon_plugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/falcon_apispec/falcon_plugin.py b/falcon_apispec/falcon_plugin.py index 4688e93..52a8c81 100644 --- a/falcon_apispec/falcon_plugin.py +++ b/falcon_apispec/falcon_plugin.py @@ -18,11 +18,14 @@ def _generate_resource_uri_mapping(app, resource, suffix): mapping = {} for route in routes_to_check: uri = route.uri_template + # Filter by resource so we don't have to parse all routes + check for any existing & matching suffix if route.resource == resource and ((suffix is not None and uri.endswith(suffix) is True) or suffix is None): mapping[uri] = {} if route.method_map: methods = {} for method_name, method_handler in route.method_map.items(): + # Multiple conditions to ignore the method : falcon responder, or a method that does not + # meet the suffix requirements: no suffix provided = ignore suffixed methods, and vice-versa if method_handler.__dict__.get("__module__") == "falcon.responders" or \ (suffix is not None and not method_handler.__name__.lower().endswith(suffix)) or \ (suffix is None and not method_handler.__name__.lower().endswith(method_name.lower())): @@ -42,6 +45,7 @@ def path_helper(self, operations, resource, base_path=None, suffix=None, **kwarg operations.update(yaml_utils.load_operations_from_docstring(resource.__doc__) or {}) + # In case multiple uri were found, keep the only one that has methods try: path = next(uri for uri, methods in resource_uri_mapping.items() if methods) except StopIteration: From 9b923e595bd0f94f088287d5fcb5cf4b1bffbbf1 Mon Sep 17 00:00:00 2001 From: EricM Date: Wed, 17 Feb 2021 17:50:35 +0100 Subject: [PATCH 09/11] Remove contraint on route naming --- falcon_apispec/falcon_plugin.py | 4 ++-- tests/falcon_test.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/falcon_apispec/falcon_plugin.py b/falcon_apispec/falcon_plugin.py index 52a8c81..e057913 100644 --- a/falcon_apispec/falcon_plugin.py +++ b/falcon_apispec/falcon_plugin.py @@ -18,8 +18,8 @@ def _generate_resource_uri_mapping(app, resource, suffix): mapping = {} for route in routes_to_check: uri = route.uri_template - # Filter by resource so we don't have to parse all routes + check for any existing & matching suffix - if route.resource == resource and ((suffix is not None and uri.endswith(suffix) is True) or suffix is None): + # Filter by resource so we don't have to parse all routes + if route.resource == resource: mapping[uri] = {} if route.method_map: methods = {} diff --git a/tests/falcon_test.py b/tests/falcon_test.py index ad113ad..e35b472 100644 --- a/tests/falcon_test.py +++ b/tests/falcon_test.py @@ -171,12 +171,12 @@ def test_path_with_suffix(self, app, spec_factory, suffixed_resource): "responses": {"200": {"description": "said hi"}}, } - app.add_route("/hello", suffixed_resource, suffix="hello") + app.add_route("/hi", suffixed_resource, suffix="hello") spec = spec_factory(app) spec.path(resource=suffixed_resource, suffix="hello") - assert spec._paths["/hello"]["get"] == expected + assert spec._paths["/hi"]["get"] == expected def test_path_ignore_suffix(self, app, spec_factory, suffixed_resource): expected = { @@ -194,14 +194,14 @@ def test_path_ignore_suffix(self, app, spec_factory, suffixed_resource): def test_path_suffix_all(self, app, spec_factory, suffixed_resource): app.add_route("/say", suffixed_resource) - app.add_route("/say/hello", suffixed_resource, suffix="hello") + app.add_route("/say/hi", suffixed_resource, suffix="hello") spec = spec_factory(app) spec.path(resource=suffixed_resource) spec.path(resource=suffixed_resource, suffix="hello") assert spec._paths["/say"]["get"]["description"] == "get something" - assert spec._paths["/say/hello"]["get"]["description"] == "get a greeting" + assert spec._paths["/say/hi"]["get"]["description"] == "get a greeting" def test_path_multiple_routes_same_resource(self, app, spec_factory): class HelloResource: From 7197d1f76ce7a8ea4d8689ac75c88a3dbc7327f5 Mon Sep 17 00:00:00 2001 From: EricM Date: Wed, 17 Feb 2021 18:01:18 +0100 Subject: [PATCH 10/11] Correct readme example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 17d09d5..f257089 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ class SuffixedHelloResource: suffixed_resource = SuffixedHelloResource() app.add_route("/say", suffixed_resource) -app.add_route("/say/hello", suffixed_resource, suffix="hello") +app.add_route("/say/hi", suffixed_resource, suffix="hello") spec = spec_factory(app) spec.path(resource=suffixed_resource) # registers on_get From dfb92dc7642df22b4c6e5870734365f1e542bf03 Mon Sep 17 00:00:00 2001 From: EricM Date: Thu, 18 Feb 2021 09:20:41 +0100 Subject: [PATCH 11/11] Added test for missing covered line --- tests/falcon_test.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/falcon_test.py b/tests/falcon_test.py index e35b472..a69b3d0 100644 --- a/tests/falcon_test.py +++ b/tests/falcon_test.py @@ -2,6 +2,7 @@ import pytest from apispec import APISpec from apispec.exceptions import APISpecError +from unittest.mock import MagicMock from falcon_apispec import FalconPlugin @@ -117,6 +118,22 @@ class HelloResource: assert spec._paths["/hi"]["x-extension"] == "global metadata" + def test_resource_no_methods(self, app, spec_factory): + class HelloResource: + """Greeting API. + --- + x-extension: global metadata + """ + + hello_resource = HelloResource() + magic_route = MagicMock(uri_template="/hi", resource=hello_resource, method_map=[]) + app._router._roots.append(magic_route) + + spec = spec_factory(app) + spec.path(resource=hello_resource) + + assert spec._paths["/hi"]["x-extension"] == "global metadata" + def test_unredundant_basepath_resource_with_slash(self, app, spec_factory): class HelloResource: def on_get(self, req, resp):