diff --git a/aspen/http/resource.py b/aspen/http/resource.py index 1ae50b75..4b8a80db 100644 --- a/aspen/http/resource.py +++ b/aspen/http/resource.py @@ -3,8 +3,6 @@ from ..exceptions import NegotiationFailure from ..output import Output -from ..request_processor.dispatcher import NotFound -from ..simplates.simplate import Simplate class Static(object): @@ -28,18 +26,13 @@ def render(self, context): return Output(body=self.raw, media_type=self.media_type, charset=self.charset) -class Dynamic(Simplate): +class Dynamic(object): """Model a dynamic HTTP resource using simplates. Make .request_processor available as it has been historically. """ - def __init__(self, request_processor, fs, raw, fs_media_type): - self.request_processor = request_processor - self.fs_media_type = fs_media_type - defaults = request_processor.simplate_defaults - default_media_type = fs_media_type or request_processor.media_type_default - super(Dynamic, self).__init__(defaults, fs, raw, default_media_type) + available_types = [] # populate in your subclass def render(self, state): """Render the resource with the given state as context, return Output. @@ -56,6 +49,8 @@ def render(self, state): Note that we don't always respect the `Accept` header (the spec says we can ignore it: ). """ + from ..request_processor.dispatcher import NotFound + available = self.available_types # When there is an extension in the URI path, the dispatcher gives us the # corresponding media type (or an empty string if unknown) @@ -75,7 +70,7 @@ def render(self, state): elif len(available) == 1: # If there's only one available type and no extension in the path, # then we ignore the Accept header - return super(Dynamic, self).render(available[0], state) + return self.render_for_type(available[0], state) else: accept = state.get('accept_header') if accept: @@ -85,11 +80,11 @@ def render(self, state): # Unparseable accept header best_match = None if best_match: - return super(Dynamic, self).render(best_match, state) + return self.render_for_type(best_match, state) elif best_match == '': if dispatch_accept is not None: # e.g. client requested `/foo.json` but `/foo.spt` has no JSON page raise NotFound() raise NegotiationFailure(accept, available) # Fall back to the first available type - return super(Dynamic, self).render(available[0], state) + return self.render_for_type(available[0], state) diff --git a/aspen/request_processor/__init__.py b/aspen/request_processor/__init__.py index dcda8b8d..f0b201a4 100644 --- a/aspen/request_processor/__init__.py +++ b/aspen/request_processor/__init__.py @@ -12,9 +12,8 @@ from algorithm import Algorithm from .typecasting import defaults as default_typecasters +from ..http.resource import Static from ..configuration import ConfigurationError, configure, parse -from ..simplates.renderers import factories -from ..simplates.simplate import SimplateDefaults default_indices = lambda: ['index.html', 'index.json', 'index', @@ -133,20 +132,23 @@ def safe_getcwd(errorstr): self.www_root = os.path.realpath(self.www_root) - # load renderers - self.renderer_factories = factories(self) + # kludge simplates -- should move out into a simplate plugin + from ..simplates.renderers import factories + from ..simplates.simplate import Simplate, SimplateDefaults + Simplate.renderer_factories = factories(self) + Simplate.default_renderers_by_media_type = defaultdict(lambda: self.renderer_default) + Simplate.default_renderers_by_media_type[self.media_type_json] = 'json_dump' - self.default_renderers_by_media_type = defaultdict(lambda: self.renderer_default) - self.default_renderers_by_media_type[self.media_type_json] = 'json_dump' - - # simplate defaults initial_context = { 'request_processor': self } - self.simplate_defaults = SimplateDefaults( - self.default_renderers_by_media_type, - self.renderer_factories, + Simplate.defaults = SimplateDefaults( + Simplate.default_renderers_by_media_type, + Simplate.renderer_factories, initial_context ) + # set up dynamic class mapping + self.dynamic_classes_by_file_extension = dict(spt=Simplate) + # mime.types # ========== # It turns out that init'ing mimetypes is somewhat expensive. This is @@ -161,4 +163,12 @@ def safe_getcwd(errorstr): def is_dynamic(self, fspath): """Given a filesystem path, return a boolean. """ - return fspath.endswith('.spt') + return self.get_resource_class(fspath) is not Static + + + def get_resource_class(self, fspath): + """Given a filesystem path, return a resource class. + """ + parts = fspath.split('.') + extension = parts[-1] if len(parts) > 1 else None + return self.dynamic_classes_by_file_extension.get(extension, Static) diff --git a/aspen/request_processor/dispatcher.py b/aspen/request_processor/dispatcher.py index f7396e64..e0523278 100644 --- a/aspen/request_processor/dispatcher.py +++ b/aspen/request_processor/dispatcher.py @@ -93,7 +93,7 @@ def dispatch_abstract(listnodes, is_dynamic, is_leaf, traverse, find_index, noex listnodes(joinedpath) - lists the nodes in the specified joined path - is_dynamic(node) - returns true iff the specified node is a renderable + is_dynamic(node) - returns true iff the specified node is dynamic is_leaf(node) - returns true iff the specified node is a leaf node diff --git a/aspen/resources.py b/aspen/resources.py index 42c24fcb..d06e3d00 100644 --- a/aspen/resources.py +++ b/aspen/resources.py @@ -16,7 +16,7 @@ import traceback from .exceptions import LoadError -from .http.resource import Dynamic, Static +from .http.resource import Static __cache__ = dict() # cache, keyed to filesystem path @@ -87,7 +87,7 @@ def load(request_processor, fspath, mtime): """Given a RequestProcessor, an fspath, and an mtime, return a Resource object (w/o caching). """ - is_dynamic = request_processor.is_dynamic(fspath) + Class = request_processor.get_resource_class(fspath) # Load bytes. # =========== @@ -103,7 +103,7 @@ def load(request_processor, fspath, mtime): # For a negotiated resource we will ignore this. guess_with = fspath - if is_dynamic: + 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': @@ -113,5 +113,4 @@ def load(request_processor, fspath, mtime): # ================================ # An instantiated resource is compiled as far as we can take it. - Class = Dynamic if is_dynamic else Static return Class(request_processor, fspath, raw, fs_media_type) diff --git a/aspen/simplates/simplate.py b/aspen/simplates/simplate.py index 483cce63..d48c1864 100644 --- a/aspen/simplates/simplate.py +++ b/aspen/simplates/simplate.py @@ -8,6 +8,7 @@ from ..output import Output from .pagination import split_and_escape, parse_specline, Page +from aspen.http.resource import Dynamic renderer_re = re.compile(r'[a-z0-9.-_]+$') media_type_re = re.compile(r'[A-Za-z0-9.+*-]+/[A-Za-z0-9.+*-]+$') @@ -75,25 +76,25 @@ def __init__(self, renderers_by_media_type, renderer_factories, initial_context) self.initial_context = initial_context # type: Dict[str, object] -class Simplate(object): +class Simplate(Dynamic): """A simplate is a dynamic resource with multiple syntaxes in one file. """ - def __init__(self, defaults, fs, raw, default_media_type): + defaults = None # type: SimplateDefaults + + def __init__(self, request_processor, fs, raw, fs_media_type): """Instantiate a simplate. - defaults - a SimplateDefaults object fs - path to this simplate raw - raw content of this simplate as bytes decoded - content of this simplate as unicode - default_media_type - the default content_type of this simplate + fs_media_type - the filesystem content_type of this simplate """ - - self.defaults = defaults # type: SimplateDefaults + self.request_processor = request_processor self.fs = fs # type: str self.raw = raw # type: bytes self.decoded = _decode(raw) # type: str - self.default_media_type = default_media_type # type: str + self.default_media_type = fs_media_type or request_processor.media_type_default self.renderers = {} # mapping of media type to Renderer objects self.available_types = [] # ordered sequence of media types @@ -101,7 +102,7 @@ def __init__(self, defaults, fs, raw, default_media_type): self.pages = self.compile_pages(pages) - def render(self, media_type, context): + def render_for_type(self, media_type, context): """ Get the response to a request for this page:: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 8ff5ff51..ea4928cf 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -66,6 +66,7 @@ def test_user_can_set_renderer_default(harness): [----] Greetings, {name}! """ + harness.request_processor.renderer_default="stdlib_format" harness.fs.www.mk(('index.html.spt', SIMPLATE),) actual = harness.simple(filepath=None, uripath='/', want='output.text') diff --git a/tests/test_dynamic_resource.py b/tests/test_dynamic_resource.py index ed574c2c..2e0a1454 100644 --- a/tests/test_dynamic_resource.py +++ b/tests/test_dynamic_resource.py @@ -6,7 +6,7 @@ from pytest import raises, yield_fixture from aspen.exceptions import NegotiationFailure -from aspen.http.resource import Dynamic +from aspen.simplates.simplate import Simplate from aspen.request_processor.dispatcher import mimetypes, NotFound from aspen.simplates.pagination import Page from aspen.simplates.renderers.stdlib_template import Factory as TemplateFactory @@ -22,7 +22,7 @@ def get(**_kw): , fs_media_type = '' ) kw.update(_kw) - return Dynamic(**kw) + return Simplate(**kw) yield get @@ -31,8 +31,8 @@ def test_dynamic_resource_is_instantiable(harness): fs = '' raw = b'[---]\n[---] text/plain via stdlib_template\n' media_type = '' - actual = Dynamic(request_processor, fs, raw, media_type).__class__ - assert actual is Dynamic + actual = Simplate(request_processor, fs, raw, media_type).__class__ + assert actual is Simplate # compile_page @@ -85,21 +85,21 @@ def test_parse_specline_enforces_order(get): def test_parse_specline_obeys_default_by_media_type(get): resource = get() - resource.request_processor.default_renderers_by_media_type['media/type'] = 'glubber' + 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() - resource.request_processor.default_renderers_by_media_type.default_factory = lambda: 'glubber' + 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() - resource.request_processor.default_renderers_by_media_type['media/type'] = 'glubber' + resource.default_renderers_by_media_type['media/type'] = 'glubber' err = raises( SyntaxError , resource._get_renderer_factory , 'media/type' @@ -179,21 +179,31 @@ def render_content(self, context): class GlubberFactory(Factory): Renderer = Glubber -def install_glubber(harness): - harness.request_processor.renderer_factories['glubber'] = \ - GlubberFactory(harness.request_processor) - harness.request_processor.default_renderers_by_media_type['text/plain'] = 'glubber' +class glubber: + + def __init__(self, harness): + self.harness = harness + + def __enter__(self): + Simplate.renderer_factories['glubber'] = GlubberFactory(self.harness.request_processor) + self.__old = Simplate.default_renderers_by_media_type['text/plain'] + Simplate.default_renderers_by_media_type['text/plain'] = 'glubber' + + def __exit__(self, *a, **kw): + del Simplate.renderer_factories['glubber'] + Simplate.default_renderers_by_media_type['text/plain'] = self.__old + def test_can_override_default_renderers_by_mimetype(harness): - install_glubber(harness) - harness.fs.www.mk(('index.spt', SIMPLATE),) - output = harness.simple(filepath='index.spt', contents=SIMPLATE, accept_header='text/plain') - assert output.text == "glubber" + with glubber(harness): + harness.fs.www.mk(('index.spt', SIMPLATE),) + output = harness.simple(filepath='index.spt', contents=SIMPLATE, accept_header='text/plain') + assert output.text == "glubber" def test_can_override_default_renderer_entirely(harness): - install_glubber(harness) - output = harness.simple(filepath='index.spt', contents=SIMPLATE, accept_header='text/plain') - assert output.text == "glubber" + with glubber(harness): + output = harness.simple(filepath='index.spt', contents=SIMPLATE, accept_header='text/plain') + assert output.text == "glubber" # indirect diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 78e9e88e..1f2692e2 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -9,6 +9,7 @@ from aspen.simplates import json_ from aspen.simplates.renderers import Factory, Renderer +from aspen.simplates.simplate import Simplate def test_a_custom_renderer(harness): @@ -28,16 +29,19 @@ def compile_meta(self, configuration): return 'foobar' request_processor = harness.request_processor - request_processor.renderer_factories['lorem'] = TestFactory(request_processor) + try: + Simplate.renderer_factories['lorem'] = TestFactory(request_processor) - r = harness.simple("[---]\n[---] text/html via lorem\nLorem ipsum") - assert r.text - d = json_.loads(r.text) - assert d['meta'] == 'foobar' - assert d['raw'] == 'Lorem ipsum' - assert d['media_type'] == 'text/html' - assert d['offset'] == 2 - assert d['compiled'] == 'LOREM IPSUM' + r = harness.simple("[---]\n[---] text/html via lorem\nLorem ipsum") + assert r.text + d = json_.loads(r.text) + assert d['meta'] == 'foobar' + assert d['raw'] == 'Lorem ipsum' + assert d['media_type'] == 'text/html' + assert d['offset'] == 2 + assert d['compiled'] == 'LOREM IPSUM' + finally: + del Simplate.renderer_factories['lorem'] def test_renderer_padding_works_with_padded_output(harness): @@ -54,10 +58,13 @@ class TestFactory(Factory): Renderer = TestRenderer request_processor = harness.request_processor - request_processor.renderer_factories['x'] = TestFactory(request_processor) + try: + Simplate.renderer_factories['x'] = TestFactory(request_processor) - output = harness.simple("[---]\n[---] text/plain via x\nSome text") - assert output.text == '\nSome text\n' + output = harness.simple("[---]\n[---] text/plain via x\nSome text") + assert output.text == '\nSome text\n' + finally: + del Simplate.renderer_factories['x'] def test_renderer_padding_works_with_stripped_output(harness): @@ -74,10 +81,14 @@ class TestFactory(Factory): Renderer = TestRenderer request_processor = harness.request_processor - request_processor.renderer_factories['y'] = TestFactory(request_processor) + try: + Simplate.renderer_factories['y'] = TestFactory(request_processor) + + output = harness.simple("[---]\n[---] text/plain via y\nSome text") + assert output.text == '\nSome text\n' + finally: + del Simplate.renderer_factories['y'] - output = harness.simple("[---]\n[---] text/plain via y\nSome text") - assert output.text == '\nSome text\n' def test_renderer_padding_achieves_correct_line_numbers_in_tracebacks(harness):