Skip to content

Commit 0771716

Browse files
authored
Merge pull request #65 from psf/flasgger-optional
Make flasgger an optional dependency
2 parents c4aa016 + c0e3cd0 commit 0771716

3 files changed

Lines changed: 112 additions & 72 deletions

File tree

httpbin/core.py

Lines changed: 84 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@
3333
except ImportError: # werkzeug < 2.1
3434
from werkzeug.wrappers import BaseResponse as Response
3535

36-
from flasgger import Swagger, NO_SANITIZER
36+
try:
37+
from flasgger import Swagger, NO_SANITIZER
38+
except ImportError:
39+
Swagger = False
3740

3841
from . import filters
3942
from .helpers import (
@@ -95,77 +98,83 @@ def jsonify(*args, **kwargs):
9598

9699
app.config["SWAGGER"] = {"title": "httpbin.org", "uiversion": 3}
97100

98-
template = {
99-
"swagger": "2.0",
100-
"info": {
101-
"title": "httpbin.org",
102-
"description": (
103-
"A simple HTTP Request & Response Service."
104-
"<br/> A <a href='http://kennethreitz.com/'>Kenneth Reitz</a> project."
105-
"<br/> <br/> <b>Run locally: </b> <br/> "
106-
"<code>$ docker pull ghcr.io/psf/httpbin</code> <br/>"
107-
"<code>$ docker run -p 80:8080 ghcr.io/psf/httpbin</code>"
108-
),
109-
"contact": {
110-
"responsibleOrganization": "Python Software Foundation",
111-
"responsibleDeveloper": "Kenneth Reitz",
112-
"url": "https://github.com/psf/httpbin/",
113-
},
114-
# "termsOfService": "http://me.com/terms",
115-
"version": version,
116-
},
117-
"host": "httpbin.org", # overrides localhost:5000
118-
"basePath": "/", # base bash for blueprint registration
119-
"schemes": ["https"],
120-
"protocol": "https",
121-
"tags": [
122-
{
123-
"name": "HTTP Methods",
124-
"description": "Testing different HTTP verbs",
125-
# 'externalDocs': {'description': 'Learn more', 'url': 'https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html'}
126-
},
127-
{"name": "Auth", "description": "Auth methods"},
128-
{
129-
"name": "Status codes",
130-
"description": "Generates responses with given status code",
131-
},
132-
{"name": "Request inspection", "description": "Inspect the request data"},
133-
{
134-
"name": "Response inspection",
135-
"description": "Inspect the response data like caching and headers",
136-
},
137-
{
138-
"name": "Response formats",
139-
"description": "Returns responses in different data formats",
140-
},
141-
{"name": "Dynamic data", "description": "Generates random and dynamic data"},
142-
{"name": "Cookies", "description": "Creates, reads and deletes Cookies"},
143-
{"name": "Images", "description": "Returns different image formats"},
144-
{"name": "Redirects", "description": "Returns different redirect responses"},
145-
{
146-
"name": "Anything",
147-
"description": "Returns anything that is passed to request",
101+
if Swagger:
102+
template = {
103+
"swagger": "2.0",
104+
"info": {
105+
"title": "httpbin.org",
106+
"description": (
107+
"A simple HTTP Request & Response Service."
108+
"<br/> A <a href='http://kennethreitz.com/'>Kenneth Reitz</a> project."
109+
"<br/> <br/> <b>Run locally: </b> <br/> "
110+
"<code>$ docker pull ghcr.io/psf/httpbin</code> <br/>"
111+
"<code>$ docker run -p 80:8080 ghcr.io/psf/httpbin</code>"
112+
),
113+
"contact": {
114+
"responsibleOrganization": "Python Software Foundation",
115+
"responsibleDeveloper": "Kenneth Reitz",
116+
"url": "https://github.com/psf/httpbin/",
117+
},
118+
# "termsOfService": "http://me.com/terms",
119+
"version": version,
148120
},
149-
],
150-
}
151-
152-
swagger_config = {
153-
"headers": [],
154-
"specs": [
155-
{
156-
"endpoint": "spec",
157-
"route": "/spec.json",
158-
"rule_filter": lambda rule: True, # all in
159-
"model_filter": lambda tag: True, # all in
160-
}
161-
],
162-
"static_url_path": "/flasgger_static",
163-
# "static_folder": "static", # must be set by user
164-
"swagger_ui": True,
165-
"specs_route": "/",
166-
}
121+
"host": "httpbin.org", # overrides localhost:5000
122+
"basePath": "/", # base bash for blueprint registration
123+
"schemes": ["https"],
124+
"protocol": "https",
125+
"tags": [
126+
{
127+
"name": "HTTP Methods",
128+
"description": "Testing different HTTP verbs",
129+
# 'externalDocs': {'description': 'Learn more', 'url': 'https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html'}
130+
},
131+
{"name": "Auth", "description": "Auth methods"},
132+
{
133+
"name": "Status codes",
134+
"description": "Generates responses with given status code",
135+
},
136+
{"name": "Request inspection", "description": "Inspect the request data"},
137+
{
138+
"name": "Response inspection",
139+
"description": "Inspect the response data like caching and headers",
140+
},
141+
{
142+
"name": "Response formats",
143+
"description": "Returns responses in different data formats",
144+
},
145+
{"name": "Dynamic data", "description": "Generates random and dynamic data"},
146+
{"name": "Cookies", "description": "Creates, reads and deletes Cookies"},
147+
{"name": "Images", "description": "Returns different image formats"},
148+
{"name": "Redirects", "description": "Returns different redirect responses"},
149+
{
150+
"name": "Anything",
151+
"description": "Returns anything that is passed to request",
152+
},
153+
],
154+
}
155+
156+
swagger_config = {
157+
"headers": [],
158+
"specs": [
159+
{
160+
"endpoint": "spec",
161+
"route": "/spec.json",
162+
"rule_filter": lambda rule: True, # all in
163+
"model_filter": lambda tag: True, # all in
164+
}
165+
],
166+
"static_url_path": "/flasgger_static",
167+
# "static_folder": "static", # must be set by user
168+
"swagger_ui": True,
169+
"specs_route": "/",
170+
}
167171

168-
swagger = Swagger(app, sanitizer=NO_SANITIZER, template=template, config=swagger_config)
172+
swagger = Swagger(app, sanitizer=NO_SANITIZER, template=template, config=swagger_config)
173+
else:
174+
app.logger.warning(
175+
"flasgger is not installed; serving the static landing page at / "
176+
"and skipping the Swagger UI and /spec.json."
177+
)
169178

170179
# Set up Bugsnag exception tracking, if desired. To use Bugsnag, install the
171180
# Bugsnag Python client with the command "pip install bugsnag", and set the
@@ -243,8 +252,12 @@ def set_cors_headers(response):
243252
# Routes
244253
# ------
245254

255+
if Swagger:
256+
staticroute = "/legacy"
257+
else:
258+
staticroute = "/"
246259

247-
@app.route("/legacy")
260+
@app.route(staticroute)
248261
def view_landing_page():
249262
"""Generates Landing Page in legacy layout."""
250263
return render_template("index.html")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ classifiers = [
3434
dependencies = [
3535
"brotlicffi",
3636
"decorator",
37-
"flasgger",
3837
"flask >= 2.2.4",
3938
'greenlet < 3.0; python_version<"3.12"',
4039
'greenlet >= 3.0.0a1; python_version>="3.12.0rc0"',
@@ -46,6 +45,7 @@ dependencies = [
4645
[project.optional-dependencies]
4746
test = ["pytest", "tox"]
4847
mainapp = [
48+
"flasgger",
4949
"gunicorn",
5050
"gevent",
5151
]

tests/test_httpbin.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
33
import os
4+
import sys
45
import base64
6+
import subprocess
57
import unittest
68
import contextlib
79
import json
@@ -796,6 +798,31 @@ def test_etag_with_no_headers(self):
796798
self.assertEqual(response.status_code, 200)
797799
self.assertEqual(response.headers.get('ETag'), 'abc')
798800

801+
def test_index_falls_back_to_static_page_without_flasgger(self):
802+
"""When flasgger isn't installed, / serves the static legacy landing
803+
page (HTTP 200) instead of the Swagger UI, rather than 500ing.
804+
805+
flasgger is imported at module load, so this runs in a subprocess that
806+
poisons sys.modules['flasgger'] before httpbin is imported.
807+
"""
808+
code = (
809+
"import sys\n"
810+
"sys.modules['flasgger'] = None\n" # makes `import flasgger` raise ImportError
811+
"import httpbin\n"
812+
"assert httpbin.core.Swagger is False, repr(httpbin.core.Swagger)\n"
813+
"r = httpbin.app.test_client().get('/')\n"
814+
"assert r.status_code == 200, r.status_code\n"
815+
"body = r.get_data(as_text=True)\n"
816+
"assert 'httpbin(1): HTTP Client Testing Service' in body, 'legacy page not served'\n"
817+
"assert 'swagger-ui' not in body, 'swagger UI unexpectedly rendered'\n"
818+
"print('OK')\n"
819+
)
820+
result = subprocess.run(
821+
[sys.executable, "-c", code], capture_output=True, text=True
822+
)
823+
self.assertEqual(result.returncode, 0, result.stderr)
824+
self.assertIn("OK", result.stdout)
825+
799826
def test_parse_multi_value_header(self):
800827
self.assertEqual(parse_multi_value_header('xyzzy'), [ "xyzzy" ])
801828
self.assertEqual(parse_multi_value_header('"xyzzy"'), [ "xyzzy" ])

0 commit comments

Comments
 (0)