Skip to content

Commit

Permalink
Merge pull request #85 from AspenWeb/resources
Browse files Browse the repository at this point in the history
  • Loading branch information
Changaco committed Sep 15, 2019
2 parents 71788da + cd3708b commit e3e6b40
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 114 deletions.
18 changes: 12 additions & 6 deletions aspen/http/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ class Static(object):
"""Model a static HTTP resource.
"""

__slots__ = ('fspath', 'raw', 'fs_media_type', 'media_type', 'charset')

def __init__(self, request_processor, fspath, raw, fs_media_type):
assert type(raw) is bytes # sanity check
__slots__ = ('fspath', 'raw', 'media_type', 'charset')

def __init__(self, request_processor, fspath):
raw = None
read_file = (
request_processor.store_static_files_in_ram or
request_processor.charset_static
)
if read_file:
with open(fspath, 'rb') as f:
raw = f.read()
self.fspath = fspath
self.raw = raw if request_processor.store_static_files_in_ram else None
self.fs_media_type = fs_media_type
self.media_type = fs_media_type or request_processor.media_type_default
self.media_type = request_processor.guess_media_type(fspath)
self.charset = None
if request_processor.charset_static:
try:
Expand Down
22 changes: 20 additions & 2 deletions aspen/request_processor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from . import typecasting
from .dispatcher import DispatchStatus, HybridDispatcher, UserlandDispatcher
from .typecasting import defaults as default_typecasters
from .. import resources
from ..resources import Resources
from ..http.resource import Static
from ..exceptions import ConfigurationError

Expand Down Expand Up @@ -145,6 +145,9 @@ def safe_getcwd(errorstr):
if not mimetypes.inited:
mimetypes.init()

# create the resources cache
self.resources = Resources(self)


def dispatch(self, path):
"""Call the dispatcher and inject the path variables into the given path object.
Expand Down Expand Up @@ -182,7 +185,7 @@ def process(self, path, querystring, accept_header, context):
typecasting.apply_typecasters(self.typecasters, path, context)

if dispatch_result.match and dispatch_result.status == DispatchStatus.okay:
resource = resources.get(self, dispatch_result.match)
resource = self.resources.get(dispatch_result.match)
context['querystring'] = querystring
output = resource.render(context, dispatch_result, accept_header)
if not isinstance(output.body, bytes):
Expand All @@ -207,6 +210,21 @@ def get_resource_class(self, fspath):
return self.dynamic_classes_by_file_extension.get(extension, Static)


def guess_media_type(self, filename):
"""Guess the media type of a file by looking at its extension.
This method is a small wrapper around :func:`mimetypes.guess_type`. It
returns :attr:`~DefaultConfiguration.media_type_default` if the guessing
fails.
"""
media_type = mimetypes.guess_type(filename, strict=False)[0]
if not media_type:
media_type = self.media_type_default
elif media_type == 'application/json':
media_type = self.media_type_json
return media_type


class DefaultConfiguration(object):
"""Default configuration values.
"""
Expand Down
76 changes: 24 additions & 52 deletions aspen/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,12 @@
from __future__ import print_function
from __future__ import unicode_literals

import mimetypes
import os
import stat

from .http.resource import Static


__cache__ = dict() # cache, keyed to filesystem path


class Entry(object):
"""An entry in the global resource cache.
"""An entry in a resource cache.
"""
__slots__ = ('fspath', 'mtime', 'resource')

Expand All @@ -27,56 +21,34 @@ def __init__(self, fspath, mtime, resource):
self.resource = resource


def get(request_processor, fspath):
"""Given a RequestProcessor and a filesystem path, return a Resource object (with caching).
"""

# Get a cache Entry object.
entry = __cache__.get(fspath)

# Process the resource.
if not entry or request_processor.changes_reload:
mtime = os.stat(fspath)[stat.ST_MTIME]
if getattr(entry, 'mtime', None) != mtime: # cache miss
resource = load(request_processor, fspath)
entry = __cache__[fspath] = Entry(fspath, mtime, resource)

# Return
# ======
# The caller must take care to avoid mutating any context dictionary at
# entry.resource.pages[0].

return entry.resource


def load(request_processor, fspath):
"""Given a RequestProcessor and a filesystem path, return a Resource object (w/o caching).
class Resources(object):
"""This class implements loading resources, and caching them.
"""

Class = request_processor.get_resource_class(fspath)
__slots__ = ('request_processor', 'cache')

# Load bytes.
# ===========
# Dynamic files are loaded according to their encoding and turned into
# unicode strings internally. Static files might be binary, so we don't
# decode them.
def __init__(self, request_processor):
self.request_processor = request_processor
self.cache = {}

with open(fspath, 'rb') as fh:
raw = fh.read()
def get(self, fspath):
"""Return a resource object, with caching.
"""

# Compute a media type.
# =====================
# For a negotiated resource we will ignore this.
# Get a cache Entry object.
entry = self.cache.get(fspath)

guess_with = fspath
if Class is not Static:
guess_with = guess_with.rsplit('.', 1)[0]
fs_media_type = mimetypes.guess_type(guess_with, strict=False)[0]
if fs_media_type == 'application/json':
fs_media_type = request_processor.media_type_json
# Process the resource.
if not entry or self.request_processor.changes_reload:
mtime = os.stat(fspath)[stat.ST_MTIME]
if getattr(entry, 'mtime', None) != mtime: # cache miss
resource = self.load(fspath)
entry = self.cache[fspath] = Entry(fspath, mtime, resource)

# Compute and instantiate a class.
# ================================
# An instantiated resource is compiled as far as we can take it.
return entry.resource

return Class(request_processor, fspath, raw, fs_media_type)
def load(self, fspath):
"""Create and return a resource object, without caching.
"""
Class = self.request_processor.get_resource_class(fspath)
return Class(self.request_processor, fspath)
9 changes: 4 additions & 5 deletions aspen/simplates/simplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,6 @@ class Simplate(Dynamic):
Args:
fspath (str): the absolute filesystem path of this simplate
raw (bytes): raw content of this simplate as bytes
fs_media_type (str): the content type derived from the extension in the
simplate's filesystem name, if it has one
"""

Expand All @@ -103,13 +100,15 @@ class Simplate(Dynamic):

defaults = None # type: SimplateDefaults

def __init__(self, request_processor, fspath, raw, fs_media_type):
def __init__(self, request_processor, fspath):
self.request_processor = request_processor
self.fspath = fspath
self.default_media_type = fs_media_type or request_processor.media_type_default
self.default_media_type = request_processor.guess_media_type(fspath.rsplit('.', 1)[0])

self.renderers = {} # mapping of media type to Renderer objects
self.available_types = [] # ordered sequence of media types
with open(fspath, 'rb') as fh:
raw = fh.read()
pages = self.parse_into_pages(_decode(raw))
self.compile_pages(pages)
self.page_one, self.page_two = pages[0], pages[1]
Expand Down
3 changes: 0 additions & 3 deletions aspen/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from filesystem_tree import FilesystemTree

from . import resources
from .http.request import Path, Querystring
from .request_processor import RequestProcessor
from .request_processor.dispatcher import TestDispatcher
Expand All @@ -25,13 +24,11 @@ def teardown():
- reset the current working directory
- remove FSFIX = %{tempdir}/fsfix
- reset Aspen's global state
- clear out sys.path_importer_cache
"""
os.chdir(CWD)
# Reset some process-global caches. Hrm ...
resources.__cache__ = {}
sys.path_importer_cache = {} # see test_weird.py

teardown() # start clean
Expand Down
6 changes: 3 additions & 3 deletions tests/test_resources_generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ def check_page_content(raw, comp_pages):
that comparison is ignored.
'''

#Convert to single-element list
# Convert to single-element list
if not isinstance(comp_pages, list):
comp_pages = [comp_pages]

#Convert all non-tuples to tuples
# Convert all non-tuples to tuples
comp_pages = [item if isinstance(item, tuple) else (item, None)
for item in comp_pages]

#execute resources.split
# Split into pages
pages = list(pagination.split(raw))

assert len(pages) == len(comp_pages)
Expand Down
82 changes: 39 additions & 43 deletions tests/test_simplates.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,93 +17,89 @@


@fixture
def get(harness):
def get(**_kw):
kw = dict( request_processor = harness.request_processor
, fspath = ''
, raw = b'[---]\n[---] text/plain via stdlib_template\n'
, fs_media_type = ''
)
kw.update(_kw)
return Simplate(**kw)
def make_simplate(harness):
def get(fspath='index.spt', raw=b'[---]\n[---] text/plain via stdlib_template\n'):
harness.fs.www.mk((fspath, raw, False))
return Simplate(harness.request_processor, harness.fs.www.resolve(fspath))
yield get


def test_dynamic_resource_is_instantiable(harness):
request_processor = harness.request_processor
fspath = ''
raw = b'[---]\n[---] text/plain via stdlib_template\n'
media_type = ''
actual = Simplate(request_processor, fspath, raw, media_type).__class__
def test_dynamic_resource_is_instantiable(make_simplate):
actual = make_simplate().__class__
assert actual is Simplate


def test_duplicate_media_type_causes_SyntaxError(get):
def test_duplicate_media_type_causes_SyntaxError(make_simplate):
with raises(SyntaxError):
get(raw=b"[---]\n[---] text/plain\n[---] text/plain\n")
make_simplate(raw=b"[---]\n[---] text/plain\n[---] text/plain\n")


# compile_page

def test_compile_page_compiles_empty_page(get):
page = get().compile_page(Page('', 'text/html'))
def test_compile_page_compiles_empty_page(make_simplate):
page = make_simplate().compile_page(Page('', 'text/html'))
actual = page[0]({}), page[1]
assert actual == ('', 'text/html')

def test_compile_page_compiles_page(get):
page = get().compile_page(Page('foo bar', 'text/html'))
def test_compile_page_compiles_page(make_simplate):
page = make_simplate().compile_page(Page('foo bar', 'text/html'))
actual = page[0]({}), page[1]
assert actual == ('foo bar', 'text/html')


# _parse_specline

def test_parse_specline_parses_specline(get):
factory, media_type = get()._parse_specline('media/type via stdlib_template')
def test_parse_specline_parses_specline(make_simplate):
factory, media_type = make_simplate()._parse_specline('media/type via stdlib_template')
actual = (factory.__class__, media_type)
assert actual == (TemplateFactory, 'media/type')

def test_parse_specline_doesnt_require_renderer(get):
factory, media_type = get()._parse_specline('media/type')
def test_parse_specline_doesnt_require_renderer(make_simplate):
factory, media_type = make_simplate()._parse_specline('media/type')
actual = (factory.__class__, media_type)
assert actual == (PercentFactory, 'media/type')

def test_parse_specline_doesnt_require_media_type(get, harness):
factory, media_type = get()._parse_specline('via stdlib_template')
def test_parse_specline_doesnt_require_media_type(harness, make_simplate):
factory, media_type = make_simplate()._parse_specline('via stdlib_template')
actual = (factory.__class__, media_type)
assert actual == (TemplateFactory, harness.request_processor.media_type_default)

def test_parse_specline_raises_SyntaxError_if_renderer_is_malformed(get):
raises(SyntaxError, get()._parse_specline, 'stdlib_template media/type')
def test_parse_specline_raises_SyntaxError_if_renderer_is_malformed(make_simplate):
with raises(SyntaxError):
make_simplate()._parse_specline('stdlib_template media/type')

def test_parse_specline_raises_SyntaxError_if_media_type_is_malformed(get):
raises(SyntaxError, get()._parse_specline, 'media-type via stdlib_template')
def test_parse_specline_raises_SyntaxError_if_media_type_is_malformed(make_simplate):
with raises(SyntaxError):
make_simplate()._parse_specline('media-type via stdlib_template')

def test_parse_specline_cant_mistake_malformed_media_type_for_renderer(get):
raises(SyntaxError, get()._parse_specline, 'media-type')
def test_parse_specline_cant_mistake_malformed_media_type_for_renderer(make_simplate):
with raises(SyntaxError):
make_simplate()._parse_specline('media-type')

def test_parse_specline_cant_mistake_malformed_renderer_for_media_type(get):
raises(SyntaxError, get()._parse_specline, 'stdlib_template')
def test_parse_specline_cant_mistake_malformed_renderer_for_media_type(make_simplate):
with raises(SyntaxError):
make_simplate()._parse_specline('stdlib_template')

def test_parse_specline_enforces_order(get):
raises(SyntaxError, get()._parse_specline, 'stdlib_template via media/type')
def test_parse_specline_enforces_order(make_simplate):
with raises(SyntaxError):
make_simplate()._parse_specline('stdlib_template via media/type')

def test_parse_specline_obeys_default_by_media_type(get):
resource = get()
def test_parse_specline_obeys_default_by_media_type(make_simplate):
resource = make_simplate()
resource.default_renderers_by_media_type['media/type'] = 'glubber'
err = raises(ValueError, resource._parse_specline, 'media/type').value
msg = err.args[0]
assert msg.startswith("Unknown renderer for media/type: glubber."), msg

def test_parse_specline_obeys_default_by_media_type_default(get):
resource = get()
def test_parse_specline_obeys_default_by_media_type_default(make_simplate):
resource = make_simplate()
resource.default_renderers_by_media_type.default_factory = lambda: 'glubber'
err = raises(ValueError, resource._parse_specline, 'media/type').value
msg = err.args[0]
assert msg.startswith("Unknown renderer for media/type: glubber.")

def test_get_renderer_factory_can_raise_syntax_error(get):
resource = get()
def test_get_renderer_factory_can_raise_syntax_error(make_simplate):
resource = make_simplate()
resource.default_renderers_by_media_type['media/type'] = 'glubber'
err = raises( SyntaxError
, resource._get_renderer_factory
Expand Down

0 comments on commit e3e6b40

Please sign in to comment.