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:
Kurt Griffiths 2017-04-14 10:53:03 -06:00 committed by John Vrbanac
parent 77907f1548
commit 661c77a901
17 changed files with 970 additions and 527 deletions

View File

@ -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

BIN
docs/_static/img/logo.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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">

View File

@ -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
----------------- -----------------

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
falcon>=1.1.0
msgpack-python>=0.4.8

View File

@ -0,0 +1,3 @@
mock>=2.0.0
pytest>=3.0.6
requests>=2.13.0

View File

@ -1,2 +0,0 @@
py==1.4.31
pytest==3.0.3

View File

@ -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

View File

@ -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)

View File

@ -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
View File

@ -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
# -------------------------------------------------------------------- # --------------------------------------------------------------------