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
|
||||
- python: 2.7
|
||||
env: TOXENV=pep8
|
||||
- python: 2.7
|
||||
env: TOXENV=pep8-examples
|
||||
- python: 2.6
|
||||
env: TOXENV=py26
|
||||
- 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-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">
|
||||
|
|
|
@ -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
|
||||
-----------------
|
||||
|
||||
|
|
|
@ -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
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
22
tox.ini
|
@ -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
|
||||
# --------------------------------------------------------------------
|
||||
|
|
Loading…
Reference in New Issue