doc: Rework the tutorial (#1009)
Rework the tutorial to be more consistent, and to better explain certain principles that drive Falcon's design. Along the way, polish a few other areas of the docs where someone might land during or immediate after walking through the tutorial.
This commit is contained in:
parent
77907f1548
commit
661c77a901
|
@ -16,6 +16,8 @@ matrix:
|
||||||
# env: TOXENV=pypy3
|
# env: TOXENV=pypy3
|
||||||
- python: 2.7
|
- python: 2.7
|
||||||
env: TOXENV=pep8
|
env: TOXENV=pep8
|
||||||
|
- python: 2.7
|
||||||
|
env: TOXENV=pep8-examples
|
||||||
- python: 2.6
|
- python: 2.6
|
||||||
env: TOXENV=py26
|
env: TOXENV=py26
|
||||||
- python: 2.7
|
- python: 2.7
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
|
@ -11,7 +11,7 @@ $(document).ready(function() {
|
||||||
|
|
||||||
<div id="logo">
|
<div id="logo">
|
||||||
<div id="logo-text"><a href="{{ pathto(master_doc) }}">Falcon</a></div>
|
<div id="logo-text"><a href="{{ pathto(master_doc) }}">Falcon</a></div>
|
||||||
<a href="{{ pathto(master_doc) }}"><img src="{{ pathto('_static/img/logo.png', 1) }}" width="163" height="211" alt="Falcon Web Framework Logo"/></a>
|
<a href="{{ pathto(master_doc) }}"><img src="{{ pathto('_static/img/logo.jpg', 1) }}" width="163" height="211" alt="Falcon Web Framework Logo"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="gh-buttons">
|
<div id="gh-buttons">
|
||||||
|
|
|
@ -23,8 +23,7 @@ overriding the default serializer via
|
||||||
"+xml" suffix, the default serializer will convert the error to JSON
|
"+xml" suffix, the default serializer will convert the error to JSON
|
||||||
or XML, respectively.
|
or XML, respectively.
|
||||||
|
|
||||||
Error classes are available directly from the `falcon` package
|
All classes are available directly in the ``falcon`` package namespace::
|
||||||
namespace::
|
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
|
|
||||||
|
@ -58,6 +57,8 @@ Mixins
|
||||||
.. autoclass:: falcon.http_error.NoRepresentation
|
.. autoclass:: falcon.http_error.NoRepresentation
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. _predefined_errors:
|
||||||
|
|
||||||
Predefined Errors
|
Predefined Errors
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
|
|
@ -156,9 +156,9 @@ html_theme_options = {
|
||||||
'fixed_sidebar': False,
|
'fixed_sidebar': False,
|
||||||
'show_powered_by': False,
|
'show_powered_by': False,
|
||||||
'extra_nav_links': OrderedDict([
|
'extra_nav_links': OrderedDict([
|
||||||
('Falcon Home', 'http://falconframework.org/'),
|
('Falcon Home', 'https://falconframework.org/'),
|
||||||
('Falcon Wiki', 'https://github.com/falconry/falcon/wiki'),
|
('Falcon Wiki', 'https://github.com/falconry/falcon/wiki'),
|
||||||
('Get Help', 'community/help.html'),
|
('Get Help', '/community/help.html'),
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,17 +1,18 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
from .images import ImageSaver, Resource
|
|
||||||
|
from .images import ImageStore, Resource
|
||||||
|
|
||||||
|
|
||||||
def create_app(image_saver):
|
def create_app(image_store):
|
||||||
image_resource = Resource(image_saver)
|
image_resource = Resource(image_store)
|
||||||
api = falcon.API()
|
api = falcon.API()
|
||||||
api.add_route('/images', image_resource)
|
api.add_route('/images', image_resource)
|
||||||
return api
|
return api
|
||||||
|
|
||||||
|
|
||||||
def get_app():
|
def get_app():
|
||||||
storage_path = os.environ.get('LOOK_STORAGE', '.')
|
storage_path = os.environ.get('LOOK_STORAGE_PATH', '.')
|
||||||
image_saver = ImageSaver(storage_path)
|
image_store = ImageStore(storage_path)
|
||||||
return create_app(image_saver)
|
return create_app(image_store)
|
||||||
|
|
|
@ -1,43 +1,58 @@
|
||||||
|
import io
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import msgpack
|
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
|
import msgpack
|
||||||
|
|
||||||
|
|
||||||
class Resource(object):
|
class Resource(object):
|
||||||
|
|
||||||
def __init__(self, image_saver):
|
def __init__(self, image_store):
|
||||||
self.image_saver = image_saver
|
self._image_store = image_store
|
||||||
|
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
resp.data = msgpack.packb({'message': 'Hello world!'})
|
doc = {
|
||||||
|
'images': [
|
||||||
|
{
|
||||||
|
'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.data = msgpack.packb(doc, use_bin_type=True)
|
||||||
resp.content_type = 'application/msgpack'
|
resp.content_type = 'application/msgpack'
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
|
|
||||||
def on_post(self, req, resp):
|
def on_post(self, req, resp):
|
||||||
filename = self.image_saver.save(req.stream, req.content_type)
|
name = self._image_store.save(req.stream, req.content_type)
|
||||||
resp.status = falcon.HTTP_201
|
resp.status = falcon.HTTP_201
|
||||||
resp.location = '/images/' + filename
|
resp.location = '/images/' + name
|
||||||
|
|
||||||
|
|
||||||
class ImageSaver:
|
class ImageStore(object):
|
||||||
|
|
||||||
def __init__(self, storage_path):
|
_CHUNK_SIZE_BYTES = 4096
|
||||||
self.storage_path = storage_path
|
|
||||||
|
# Note the use of dependency injection for standard library
|
||||||
|
# methods. We'll use these later to avoid monkey-patching.
|
||||||
|
def __init__(self, storage_path, uuidgen=uuid.uuid4, fopen=io.open):
|
||||||
|
self._storage_path = storage_path
|
||||||
|
self._uuidgen = uuidgen
|
||||||
|
self._fopen = fopen
|
||||||
|
|
||||||
def save(self, image_stream, image_content_type):
|
def save(self, image_stream, image_content_type):
|
||||||
ext = mimetypes.guess_extension(image_content_type)
|
ext = mimetypes.guess_extension(image_content_type)
|
||||||
filename = '{uuid}{ext}'.format(uuid=uuid.uuid4(), ext=ext)
|
name = '{uuid}{ext}'.format(uuid=self._uuidgen(), ext=ext)
|
||||||
image_path = os.path.join(self.storage_path, filename)
|
image_path = os.path.join(self._storage_path, name)
|
||||||
|
|
||||||
with open(image_path, 'wb') as image_file:
|
with self._fopen(image_path, 'wb') as image_file:
|
||||||
while True:
|
while True:
|
||||||
chunk = image_stream.read(4096)
|
chunk = image_stream.read(self._CHUNK_SIZE_BYTES)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
|
|
||||||
image_file.write(chunk)
|
image_file.write(chunk)
|
||||||
return filename
|
|
||||||
|
return name
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
falcon==1.1.0rc1
|
|
||||||
gunicorn==19.6.0
|
|
||||||
msgpack-python==0.4.8
|
|
||||||
python-mimeparse==1.6.0
|
|
||||||
requests==2.11.1
|
|
||||||
six==1.10.0
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
falcon>=1.1.0
|
||||||
|
msgpack-python>=0.4.8
|
|
@ -0,0 +1,3 @@
|
||||||
|
mock>=2.0.0
|
||||||
|
pytest>=3.0.6
|
||||||
|
requests>=2.13.0
|
|
@ -1,2 +0,0 @@
|
||||||
py==1.4.31
|
|
||||||
pytest==3.0.3
|
|
|
@ -1,30 +1,35 @@
|
||||||
import io
|
import io
|
||||||
from unittest.mock import call, MagicMock, mock_open
|
|
||||||
|
import falcon
|
||||||
|
from falcon import testing
|
||||||
|
from mock import call, MagicMock, mock_open
|
||||||
|
import msgpack
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
import look.app
|
import look.app
|
||||||
import look.images
|
import look.images
|
||||||
|
|
||||||
import msgpack
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
import falcon
|
|
||||||
from falcon import testing
|
|
||||||
import falcon.request_helpers
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_saver():
|
def mock_store():
|
||||||
return MagicMock()
|
return MagicMock()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client(mock_saver):
|
def client(mock_store):
|
||||||
api = look.app.create_app(mock_saver)
|
api = look.app.create_app(mock_store)
|
||||||
return testing.TestClient(api)
|
return testing.TestClient(api)
|
||||||
|
|
||||||
|
|
||||||
def test_get_message(client):
|
def test_list_images(client):
|
||||||
doc = {u'message': u'Hello world!'}
|
doc = {
|
||||||
|
'images': [
|
||||||
|
{
|
||||||
|
'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
response = client.simulate_get('/images')
|
response = client.simulate_get('/images')
|
||||||
result_doc = msgpack.unpackb(response.content, encoding='utf-8')
|
result_doc = msgpack.unpackb(response.content, encoding='utf-8')
|
||||||
|
@ -35,36 +40,48 @@ def test_get_message(client):
|
||||||
|
|
||||||
# With clever composition of fixtures, we can observe what happens with
|
# With clever composition of fixtures, we can observe what happens with
|
||||||
# the mock injected into the image resource.
|
# the mock injected into the image resource.
|
||||||
def test_post_image(client, mock_saver):
|
def test_post_image(client, mock_store):
|
||||||
file_name = 'fake-image-name.xyz'
|
file_name = 'fake-image-name.xyz'
|
||||||
# we need to know what ImageSaver method will be used
|
|
||||||
mock_saver.save.return_value = file_name
|
# We need to know what ImageStore method will be used
|
||||||
|
mock_store.save.return_value = file_name
|
||||||
image_content_type = 'image/xyz'
|
image_content_type = 'image/xyz'
|
||||||
|
|
||||||
response = client.simulate_post('/images',
|
response = client.simulate_post(
|
||||||
body=b'some-fake-bytes',
|
'/images',
|
||||||
headers={'content-type': image_content_type})
|
body=b'some-fake-bytes',
|
||||||
|
headers={'content-type': image_content_type}
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status == falcon.HTTP_CREATED
|
assert response.status == falcon.HTTP_CREATED
|
||||||
assert response.headers['location'] == '/images/{}'.format(file_name)
|
assert response.headers['location'] == '/images/{}'.format(file_name)
|
||||||
saver_call = mock_saver.save.call_args
|
saver_call = mock_store.save.call_args
|
||||||
# saver_call is a unittest.mock.call tuple.
|
|
||||||
# It's first element is a tuple of positional arguments supplied when calling the mock.
|
# saver_call is a unittest.mock.call tuple. It's first element is a
|
||||||
|
# tuple of positional arguments supplied when calling the mock.
|
||||||
assert isinstance(saver_call[0][0], falcon.request_helpers.BoundedStream)
|
assert isinstance(saver_call[0][0], falcon.request_helpers.BoundedStream)
|
||||||
assert saver_call[0][1] == image_content_type
|
assert saver_call[0][1] == image_content_type
|
||||||
|
|
||||||
|
|
||||||
def test_saving_image(monkeypatch):
|
def test_saving_image(monkeypatch):
|
||||||
# this still has some mocks, but they are more localized
|
# This still has some mocks, but they are more localized and do not
|
||||||
|
# have to be monkey-patched into standard library modules (always a
|
||||||
|
# risky business).
|
||||||
mock_file_open = mock_open()
|
mock_file_open = mock_open()
|
||||||
monkeypatch.setattr('builtins.open', mock_file_open)
|
|
||||||
fake_uuid = 'blablabla'
|
fake_uuid = '123e4567-e89b-12d3-a456-426655440000'
|
||||||
monkeypatch.setattr('look.images.uuid.uuid4', lambda: fake_uuid)
|
|
||||||
|
def mock_uuidgen():
|
||||||
|
return fake_uuid
|
||||||
|
|
||||||
fake_image_bytes = b'fake-image-bytes'
|
fake_image_bytes = b'fake-image-bytes'
|
||||||
fake_request_stream = io.BytesIO(fake_image_bytes)
|
fake_request_stream = io.BytesIO(fake_image_bytes)
|
||||||
storage_path = 'fake-storage-path'
|
storage_path = 'fake-storage-path'
|
||||||
saver = look.images.ImageSaver(storage_path)
|
store = look.images.ImageStore(
|
||||||
|
storage_path,
|
||||||
|
uuidgen=mock_uuidgen,
|
||||||
|
fopen=mock_file_open
|
||||||
|
)
|
||||||
|
|
||||||
assert saver.save(fake_request_stream, 'image/png') == fake_uuid + '.png'
|
assert store.save(fake_request_stream, 'image/png') == fake_uuid + '.png'
|
||||||
assert call().write(fake_image_bytes) in mock_file_open.mock_calls
|
assert call().write(fake_image_bytes) in mock_file_open.mock_calls
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
|
import os
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
def test_posted_image_gets_saved():
|
def test_posted_image_gets_saved():
|
||||||
|
file_save_prefix = '/tmp/'
|
||||||
location_prefix = '/images/'
|
location_prefix = '/images/'
|
||||||
fake_image_bytes = b'fake-image-bytes'
|
fake_image_bytes = b'fake-image-bytes'
|
||||||
|
|
||||||
response = requests.post('http://localhost:8000/images',
|
response = requests.post(
|
||||||
data=fake_image_bytes,
|
'http://localhost:8000/images',
|
||||||
headers={'content-type': 'image/png'})
|
data=fake_image_bytes,
|
||||||
|
headers={'content-type': 'image/png'}
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
location = response.headers['location']
|
location = response.headers['location']
|
||||||
assert location.startswith(location_prefix)
|
assert location.startswith(location_prefix)
|
||||||
filename = location.replace(location_prefix, '')
|
image_name = location.replace(location_prefix, '')
|
||||||
# assuming that the storage path is "/tmp"
|
|
||||||
with open('/tmp/' + filename, 'rb') as image_file:
|
file_path = file_save_prefix + image_name
|
||||||
|
with open(file_path, 'rb') as image_file:
|
||||||
assert image_file.read() == fake_image_bytes
|
assert image_file.read() == fake_image_bytes
|
||||||
|
|
||||||
|
os.remove(file_path)
|
||||||
|
|
|
@ -12,15 +12,16 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Testing utilities.
|
"""Functional testing framework for Falcon apps and Falcon itself.
|
||||||
|
|
||||||
This package contains various test classes and utility functions to
|
Falcon's testing module contains various test classes and utility
|
||||||
support functional testing for both Falcon-based apps and the Falcon
|
functions to support functional testing for both Falcon-based apps and
|
||||||
framework itself. Both unittest-style and pytest-style tests are
|
the Falcon framework itself.
|
||||||
supported::
|
|
||||||
|
The testing framework supports both unittest and pytest::
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
# unittest-style
|
# unittest
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
from falcon import testing
|
from falcon import testing
|
||||||
|
@ -46,7 +47,7 @@ supported::
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
# pytest-style
|
# pytest
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
|
|
||||||
from falcon import testing
|
from falcon import testing
|
||||||
|
|
22
tox.ini
22
tox.ini
|
@ -66,7 +66,7 @@ basepython = python3.5
|
||||||
deps = {[with-debug-tools]deps}
|
deps = {[with-debug-tools]deps}
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# Cython
|
# ujson
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
||||||
[with-ujson]
|
[with-ujson]
|
||||||
|
@ -146,13 +146,31 @@ basepython = python2.7
|
||||||
|
|
||||||
commands = flake8 \
|
commands = flake8 \
|
||||||
--max-complexity=15 \
|
--max-complexity=15 \
|
||||||
--exclude=./build,.venv,.tox,dist,docs,.eggs,./falcon/bench/nuts \
|
--exclude=./build,.venv,.tox,dist,docs,.eggs,examples,./falcon/bench/nuts \
|
||||||
--ignore=F403 \
|
--ignore=F403 \
|
||||||
--max-line-length=99 \
|
--max-line-length=99 \
|
||||||
--import-order-style=google \
|
--import-order-style=google \
|
||||||
--application-import-names=falcon \
|
--application-import-names=falcon \
|
||||||
[]
|
[]
|
||||||
|
|
||||||
|
[testenv:pep8-examples]
|
||||||
|
deps = flake8
|
||||||
|
flake8-quotes
|
||||||
|
flake8-import-order
|
||||||
|
|
||||||
|
# NOTE(kgriffs): Run with py27 since some code branches assume the
|
||||||
|
# unicode type is defined, and pep8 complains in those cases when
|
||||||
|
# running under py3.
|
||||||
|
basepython = python2.7
|
||||||
|
|
||||||
|
commands = flake8 examples \
|
||||||
|
--max-complexity=12 \
|
||||||
|
--ignore=F403 \
|
||||||
|
--max-line-length=99 \
|
||||||
|
--import-order-style=google \
|
||||||
|
--application-import-names=look \
|
||||||
|
[]
|
||||||
|
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
# For viewing environ dicts generated by various WSGI servers
|
# For viewing environ dicts generated by various WSGI servers
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
|
|
Loading…
Reference in New Issue