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
- python: 2.7
env: TOXENV=pep8
- python: 2.7
env: TOXENV=pep8-examples
- python: 2.6
env: TOXENV=py26
- 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-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 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
or XML, respectively.
Error classes are available directly from the `falcon` package
namespace::
All classes are available directly in the ``falcon`` package namespace::
import falcon
@ -58,6 +57,8 @@ Mixins
.. autoclass:: falcon.http_error.NoRepresentation
:members:
.. _predefined_errors:
Predefined Errors
-----------------

View File

@ -156,9 +156,9 @@ html_theme_options = {
'fixed_sidebar': False,
'show_powered_by': False,
'extra_nav_links': OrderedDict([
('Falcon Home', 'http://falconframework.org/'),
('Falcon Home', 'https://falconframework.org/'),
('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 falcon
from .images import ImageSaver, Resource
from .images import ImageStore, Resource
def create_app(image_saver):
image_resource = Resource(image_saver)
def create_app(image_store):
image_resource = Resource(image_store)
api = falcon.API()
api.add_route('/images', image_resource)
return api
def get_app():
storage_path = os.environ.get('LOOK_STORAGE', '.')
image_saver = ImageSaver(storage_path)
return create_app(image_saver)
storage_path = os.environ.get('LOOK_STORAGE_PATH', '.')
image_store = ImageStore(storage_path)
return create_app(image_store)

View File

@ -1,43 +1,58 @@
import io
import mimetypes
import os
import uuid
import msgpack
import falcon
import msgpack
class Resource(object):
def __init__(self, image_saver):
self.image_saver = image_saver
def __init__(self, image_store):
self._image_store = image_store
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.status = falcon.HTTP_200
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.location = '/images/' + filename
resp.location = '/images/' + name
class ImageSaver:
class ImageStore(object):
def __init__(self, storage_path):
self.storage_path = storage_path
_CHUNK_SIZE_BYTES = 4096
# 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):
ext = mimetypes.guess_extension(image_content_type)
filename = '{uuid}{ext}'.format(uuid=uuid.uuid4(), ext=ext)
image_path = os.path.join(self.storage_path, filename)
name = '{uuid}{ext}'.format(uuid=self._uuidgen(), ext=ext)
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:
chunk = image_stream.read(4096)
chunk = image_stream.read(self._CHUNK_SIZE_BYTES)
if not chunk:
break
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
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.images
import msgpack
import pytest
import falcon
from falcon import testing
import falcon.request_helpers
@pytest.fixture
def mock_saver():
def mock_store():
return MagicMock()
@pytest.fixture
def client(mock_saver):
api = look.app.create_app(mock_saver)
def client(mock_store):
api = look.app.create_app(mock_store)
return testing.TestClient(api)
def test_get_message(client):
doc = {u'message': u'Hello world!'}
def test_list_images(client):
doc = {
'images': [
{
'href': '/images/1eaf6ef1-7f2d-4ecc-a8d5-6e8adba7cc0e.png'
}
]
}
response = client.simulate_get('/images')
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
# 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'
# 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'
response = client.simulate_post('/images',
body=b'some-fake-bytes',
headers={'content-type': image_content_type})
response = client.simulate_post(
'/images',
body=b'some-fake-bytes',
headers={'content-type': image_content_type}
)
assert response.status == falcon.HTTP_CREATED
assert response.headers['location'] == '/images/{}'.format(file_name)
saver_call = mock_saver.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 = 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.
assert isinstance(saver_call[0][0], falcon.request_helpers.BoundedStream)
assert saver_call[0][1] == image_content_type
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()
monkeypatch.setattr('builtins.open', mock_file_open)
fake_uuid = 'blablabla'
monkeypatch.setattr('look.images.uuid.uuid4', lambda: fake_uuid)
fake_uuid = '123e4567-e89b-12d3-a456-426655440000'
def mock_uuidgen():
return fake_uuid
fake_image_bytes = b'fake-image-bytes'
fake_request_stream = io.BytesIO(fake_image_bytes)
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

View File

@ -1,18 +1,26 @@
import os
import requests
def test_posted_image_gets_saved():
file_save_prefix = '/tmp/'
location_prefix = '/images/'
fake_image_bytes = b'fake-image-bytes'
response = requests.post('http://localhost:8000/images',
data=fake_image_bytes,
headers={'content-type': 'image/png'})
response = requests.post(
'http://localhost:8000/images',
data=fake_image_bytes,
headers={'content-type': 'image/png'}
)
assert response.status_code == 201
location = response.headers['location']
assert location.startswith(location_prefix)
filename = location.replace(location_prefix, '')
# assuming that the storage path is "/tmp"
with open('/tmp/' + filename, 'rb') as image_file:
image_name = location.replace(location_prefix, '')
file_path = file_save_prefix + image_name
with open(file_path, 'rb') as image_file:
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
# 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
support functional testing for both Falcon-based apps and the Falcon
framework itself. Both unittest-style and pytest-style tests are
supported::
Falcon's testing module contains various test classes and utility
functions to support functional testing for both Falcon-based apps and
the Falcon framework itself.
The testing framework supports both unittest and pytest::
# -----------------------------------------------------------------
# unittest-style
# unittest
# -----------------------------------------------------------------
from falcon import testing
@ -46,7 +47,7 @@ supported::
# -----------------------------------------------------------------
# pytest-style
# pytest
# -----------------------------------------------------------------
from falcon import testing

22
tox.ini
View File

@ -66,7 +66,7 @@ basepython = python3.5
deps = {[with-debug-tools]deps}
# --------------------------------------------------------------------
# Cython
# ujson
# --------------------------------------------------------------------
[with-ujson]
@ -146,13 +146,31 @@ basepython = python2.7
commands = flake8 \
--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 \
--max-line-length=99 \
--import-order-style=google \
--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
# --------------------------------------------------------------------