-
Notifications
You must be signed in to change notification settings - Fork 38
/
__init__.py
204 lines (149 loc) · 5.79 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
"""
aspen.resources
+++++++++++++++
Aspen uses resources to model HTTP resources.
Here is the class hierarchy:
Resource Logic Pages Content Pages
+-- DynamicResource -----------------------------
| +-- NegotiatedResource 2 1 or more
| | +-- RenderedResource 1 or 2 1
+-- StaticResource 0 1
The call chain looks like this for static resources:
StaticResource.respond(request, response)
It's more complicate for dynamic resources:
DynamicResource.respond
DynamicResource.parse
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import mimetypes
import os
import re
import stat
import sys
import traceback
from aspen.backcompat import StringIO
from aspen.exceptions import LoadError
from aspen.resources.negotiated_resource import NegotiatedResource
from aspen.resources.rendered_resource import RenderedResource
from aspen.resources.static_resource import StaticResource
# Cache helpers
# =============
__cache__ = dict() # cache, keyed to filesystem path
class Entry:
"""An entry in the global resource cache.
"""
fspath = '' # The filesystem path [string]
mtime = None # The timestamp of the last change [int]
quadruple = None # A post-processed version of the data [4-tuple]
exc = None # Any exception in reading or compilation [Exception]
def __init__(self):
self.fspath = ''
self.mtime = 0
self.quadruple = ()
def decode_raw(raw):
"""As per PEP 263, decode raw data according to the encoding specified in
the first couple lines of the data, or in ASCII. Non-ASCII data without
an encoding specified will cause UnicodeDecodeError to be raised.
"""
decl_re = re.compile(r'^[ \t\f]*#.*coding[:=][ \t]*([-\w.]+)')
def get_declaration(line):
match = decl_re.match(line)
if match:
return match.group(1)
return None
encoding = None
fulltext = b''
sio = StringIO(raw)
for line in (sio.readline(), sio.readline()):
potential = get_declaration(line)
if potential is not None:
if encoding is None:
# If both lines match, use the first. This matches Python's
# observed behavior.
encoding = potential
munged = b'# encoding set to {0}\n'.format(encoding)
else:
# But always munge any encoding line. We can't simply remove
# the line, because we want to preserve the line numbering.
# However, later on when we ask Python to exec a unicode
# object, we'll get a SyntaxError if we have a well-formed
# `coding: # ` line in it.
munged = b'# encoding NOT set to {0}\n'.format(potential)
line = line.split(b'#')[0] + munged
fulltext += line
fulltext += sio.read()
sio.close()
return fulltext.decode(encoding or b'ascii')
# Core loaders
# ============
def load(website, fspath, mtime):
"""Given a Request and a mtime, return a Resource object (w/o caching).
"""
is_spt = fspath.endswith('.spt')
# Load bytes.
# ===========
# .spt files are simplates, which get loaded according to their encoding
# and turned into unicode strings internally
# non-.spt files are static, possibly binary, so don't get decoded
with open(fspath, 'rb') as fh:
raw = fh.read()
if is_spt:
raw = decode_raw(raw)
# Compute a media type.
# =====================
# For a negotiated resource we will ignore this.
guess_with = fspath
if is_spt:
guess_with = guess_with[:-4]
fs_media_type = mimetypes.guess_type(guess_with, strict=False)[0]
if fs_media_type is None:
media_type = website.media_type_default
else:
media_type = fs_media_type
# Compute and instantiate a class.
# ================================
# An instantiated resource is compiled as far as we can take it.
if not is_spt: # static
Class = StaticResource
elif fs_media_type is not None: # rendered
Class = RenderedResource
else: # negotiated
Class = NegotiatedResource
resource = Class(website, fspath, raw, media_type, mtime)
return resource
def get(website, fspath):
"""Given a website and a filesystem path, return a Resource object (with caching).
"""
# XXX This is not thread-safe. It used to be, but then I simplified it
# when I switched to diesel. Now that we have multiple engines, some of
# which are threaded, we need to make this thread-safe again.
# Get a cache Entry object.
# =========================
if fspath not in __cache__:
entry = Entry()
__cache__[fspath] = entry
entry = __cache__[fspath]
# Process the resource.
# =====================
mtime = os.stat(fspath)[stat.ST_MTIME]
if entry.mtime == mtime: # cache hit
if entry.exc is not None:
raise entry.exc
else: # cache miss
try:
entry.resource = load(website, fspath, mtime)
except: # capture any Exception
entry.exc = (LoadError(traceback.format_exc()), sys.exc_info()[2])
else: # reset any previous Exception
entry.exc = None
entry.mtime = mtime
if entry.exc is not None:
raise entry.exc[0] # TODO Why [0] here, and not above?
# Return
# ======
# The caller must take care to avoid mutating any context dictionary at
# entry.resource.pages[0].
return entry.resource