From d526545b9ef75e8e75b58d8eaa95aadb31dd6560 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 2 Sep 2021 21:29:44 +0300 Subject: [PATCH 1/3] Support remote OpenAPI specifications This adds support to fetch OpenAPI specifications over the internet to be rendered by the sphinxcontrib-openapi. --- sphinxcontrib/openapi/__main__.py | 5 ++-- sphinxcontrib/openapi/directive.py | 42 +++++++++++++++++++++--------- tests/test_openapi.py | 32 ++++++++++++++++++----- 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/sphinxcontrib/openapi/__main__.py b/sphinxcontrib/openapi/__main__.py index e7530a5..cdc55df 100644 --- a/sphinxcontrib/openapi/__main__.py +++ b/sphinxcontrib/openapi/__main__.py @@ -59,8 +59,9 @@ def main(): if options.group: openapi_options['group'] = True - openapi_options.setdefault('uri', 'file://%s' % options.input) - spec = directive._get_spec(options.input, options.encoding) + uri = directive._get_spec_uri(options.input) + openapi_options.setdefault('uri', uri.geturl()) + spec = directive._get_spec(uri, options.encoding) renderer = renderers.HttpdomainOldRenderer(None, openapi_options) for line in renderer.render_restructuredtext_markup(spec): diff --git a/sphinxcontrib/openapi/directive.py b/sphinxcontrib/openapi/directive.py index d42785a..0f094c5 100644 --- a/sphinxcontrib/openapi/directive.py +++ b/sphinxcontrib/openapi/directive.py @@ -12,16 +12,33 @@ from docutils.parsers.rst import directives from sphinx.util.docutils import SphinxDirective +from urllib.parse import urlparse import yaml - +try: + import requests + _requests = requests +except ImportError: + _requests = None # Locally cache spec to speedup processing of same spec file in multiple # openapi directives @functools.lru_cache() -def _get_spec(abspath, encoding): - with open(abspath, 'rt', encoding=encoding) as stream: - return yaml.safe_load(stream) +def _get_spec(uri, encoding): + if uri.scheme == 'http' or uri.scheme == 'https': + r = _requests.get(uri.geturl()) + return yaml.safe_load(r.text.encode(encoding)) + else: + with open(uri.path, 'rt', encoding=encoding) as stream: + return yaml.safe_load(stream) +def _get_spec_uri(arg): + try: + ret = urlparse(arg) + return ret + except: + path = directives.path(arg) + ret = urlparse("file://{}".format(path)) + return ret def create_directive_from_renderer(renderer_cls): """Create rendering directive from a renderer class.""" @@ -37,22 +54,21 @@ class _RenderingDirective(SphinxDirective): ) def run(self): - relpath, abspath = self.env.relfn2path(directives.path(self.arguments[0])) - + uri = _get_spec_uri(self.arguments[0]) # URI parameter is crucial for resolving relative references. So we # need to set this option properly as it's used later down the # stack. - self.options.setdefault('uri', 'file://%s' % abspath) - - # Add a given OpenAPI spec as a dependency of the referring - # reStructuredText document, so the document is rebuilt each time - # the spec is changed. - self.env.note_dependency(relpath) + self.options.setdefault('uri', uri.geturl()) + if uri.scheme == 'file': + # Add a given OpenAPI spec as a dependency of the referring + # reStructuredText document, so the document is rebuilt each time + # the spec is changed. + self.env.note_dependency(uri.path) # Read the spec using encoding passed to the directive or fallback to # the one specified in Sphinx's config. encoding = self.options.get('encoding', self.config.source_encoding) - spec = _get_spec(abspath, encoding) + spec = _get_spec(uri, encoding) return renderer_cls(self.state, self.options).render(spec) return _RenderingDirective diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 103e00d..228fa73 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -1818,30 +1818,48 @@ def test_noproperties(self): def test_openapi2_examples(tmpdir, run_sphinx): spec = os.path.join( os.path.abspath(os.path.dirname(__file__)), - 'OpenAPI-Specification', - 'examples', + 'testspecs', 'v2.0', - 'json', 'uber.json') py.path.local(spec).copy(tmpdir.join('src', 'test-spec.yml')) with pytest.raises(ValueError) as excinfo: - run_sphinx('test-spec.yml', options={'examples': True}) + run_sphinx(tmpdir.join('src', 'test-spec.yml'), options={'examples': True}) assert str(excinfo.value) == ( 'Rendering examples is not supported for OpenAPI v2.x specs.') +def test_openapi2_url(run_sphinx): + with pytest.raises(ValueError) as excinfo: + run_sphinx('https://petstore.swagger.io/v2/swagger.json', options={'examples': True}) + + assert str(excinfo.value) == ( + 'Rendering examples is not supported for OpenAPI v2.x specs.') + @pytest.mark.parametrize('render_examples', [False, True]) def test_openapi3_examples(tmpdir, run_sphinx, render_examples): spec = os.path.join( os.path.abspath(os.path.dirname(__file__)), - 'OpenAPI-Specification', - 'examples', + 'testspecs', 'v3.0', 'petstore.yaml') py.path.local(spec).copy(tmpdir.join('src', 'test-spec.yml')) - run_sphinx('test-spec.yml', options={'examples': render_examples}) + run_sphinx(tmpdir.join('src', 'test-spec.yml'), options={'examples': render_examples}) + + rendered_html = tmpdir.join('out', 'index.html').read_text('utf-8') + + assert ('Example response:' in rendered_html) \ + == render_examples + +@pytest.mark.parametrize('render_examples', [False, True]) +def test_openapi3_url(tmpdir, run_sphinx, render_examples): + spec = os.path.join( + os.path.abspath(os.path.dirname(__file__)), + 'testspecs', + 'v3.0', + 'petstore.yaml') + run_sphinx('https://petstore3.swagger.io/api/v3/openapi.json', options={'examples': render_examples}) rendered_html = tmpdir.join('out', 'index.html').read_text('utf-8') From cf908c1bdba25b618bfdbb0fabb1204de188c99d Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 2 Sep 2021 21:34:50 +0300 Subject: [PATCH 2/3] Add documentation for fetching remote specs --- docs/index.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 5f60b6f..14b833e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -35,6 +35,11 @@ and it will be rendered into something like: .. openapi:: specs/openapi.yml +You can also use URLs where to fetch the OpenAPI spec from: + +.. code:: restructuredtext + + .. openapi:: https://petstore.swagger.io/v2/swagger.json Options ======= From 944c48cb7b3b03bcea6d5950a4169905c9214803 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 13 Jan 2023 12:58:13 +0000 Subject: [PATCH 3/3] Fix style issues Signed-off-by: Stephen Finucane --- sphinxcontrib/openapi/directive.py | 5 ++++- tests/test_openapi.py | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/sphinxcontrib/openapi/directive.py b/sphinxcontrib/openapi/directive.py index 0f094c5..235a50c 100644 --- a/sphinxcontrib/openapi/directive.py +++ b/sphinxcontrib/openapi/directive.py @@ -20,6 +20,7 @@ except ImportError: _requests = None + # Locally cache spec to speedup processing of same spec file in multiple # openapi directives @functools.lru_cache() @@ -31,15 +32,17 @@ def _get_spec(uri, encoding): with open(uri.path, 'rt', encoding=encoding) as stream: return yaml.safe_load(stream) + def _get_spec_uri(arg): try: ret = urlparse(arg) return ret - except: + except Exception: path = directives.path(arg) ret = urlparse("file://{}".format(path)) return ret + def create_directive_from_renderer(renderer_cls): """Create rendering directive from a renderer class.""" diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 228fa73..f544978 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -1837,6 +1837,7 @@ def test_openapi2_url(run_sphinx): assert str(excinfo.value) == ( 'Rendering examples is not supported for OpenAPI v2.x specs.') + @pytest.mark.parametrize('render_examples', [False, True]) def test_openapi3_examples(tmpdir, run_sphinx, render_examples): spec = os.path.join( @@ -1852,14 +1853,17 @@ def test_openapi3_examples(tmpdir, run_sphinx, render_examples): assert ('Example response:' in rendered_html) \ == render_examples + @pytest.mark.parametrize('render_examples', [False, True]) def test_openapi3_url(tmpdir, run_sphinx, render_examples): - spec = os.path.join( + os.path.join( os.path.abspath(os.path.dirname(__file__)), 'testspecs', 'v3.0', 'petstore.yaml') - run_sphinx('https://petstore3.swagger.io/api/v3/openapi.json', options={'examples': render_examples}) + run_sphinx( + 'https://petstore3.swagger.io/api/v3/openapi.json', + options={'examples': render_examples}) rendered_html = tmpdir.join('out', 'index.html').read_text('utf-8')