[关闭]
@xuchongfeng 2015-05-26T01:42:02.000000Z 字数 51496 阅读 161
  1. #!/usr/bin/env python
  2. """web.py: makes web apps (http://webpy.org)"""
  3. __version__ = "0.1381"
  4. __revision__ = "$Rev: 72 $"
  5. __license__ = "public domain"
  6. __author__ = "Aaron Swartz <me@aaronsw.com>"
  7. __contributors__ = "see http://webpy.org/changes"
  8. from __future__ import generators
  9. # long term todo:
  10. # - new form system
  11. # - new templating system
  12. # - unit tests?
  13. # todo:
  14. # - get rid of upvars
  15. # - break up into separate files
  16. # - provide an option to use .write()
  17. # - allow people to do $self.id from inside a reparam
  18. # - add sqlite support
  19. # - convert datetimes, floats in WebSafe
  20. # - locks around memoize
  21. # - fix memoize to use cacheify style techniques
  22. # - merge curval query with the insert
  23. # - figure out how to handle squid, etc. for web.ctx.ip
  24. import os, os.path, sys, time, types, traceback, threading
  25. import cgi, re, urllib, urlparse, Cookie, pprint
  26. from threading import currentThread
  27. from tokenize import tokenprog
  28. iters = (list, tuple)
  29. if hasattr(__builtins__, 'set') or (
  30. hasattr(__builtins__, 'has_key') and __builtins__.has_key('set')):
  31. iters += (set,)
  32. try:
  33. from sets import Set
  34. iters += (Set,)
  35. except ImportError:
  36. pass
  37. try:
  38. import datetime, itertools
  39. except ImportError:
  40. pass
  41. try:
  42. from Cheetah.Compiler import Compiler
  43. from Cheetah.Filters import Filter
  44. _hasTemplating = True
  45. except ImportError:
  46. _hasTemplating = False
  47. try:
  48. from DBUtils.PooledDB import PooledDB
  49. _hasPooling = True
  50. except ImportError:
  51. _hasPooling = False
  52. # hack for compatibility with Python 2.3:
  53. if not hasattr(traceback, 'format_exc'):
  54. from cStringIO import StringIO
  55. def format_exc(limit=None):
  56. strbuf = StringIO()
  57. traceback.print_exc(limit, strbuf)
  58. return strbuf.getvalue()
  59. traceback.format_exc = format_exc
  60. ## General Utilities
  61. def _strips(direction, text, remove):
  62. if direction == 'l':
  63. if text.startswith(remove):
  64. return text[len(remove):]
  65. elif direction == 'r':
  66. if text.endswith(remove):
  67. return text[:-len(remove)]
  68. else:
  69. raise ValueError, "Direction needs to be r or l."
  70. return text
  71. def rstrips(text, remove):
  72. """removes the string `remove` from the right of `text`"""
  73. return _strips('r', text, remove)
  74. def lstrips(text, remove):
  75. """removes the string `remove` from the left of `text`"""
  76. return _strips('l', text, remove)
  77. def strips(text, remove):
  78. """removes the string `remove` from the both sides of `text`"""
  79. return rstrips(lstrips(text, remove), remove)
  80. def autoassign(self, locals):
  81. """
  82. Automatically assigns local variables to `self`.
  83. Generally used in `__init__` methods, as in:
  84. def __init__(self, foo, bar, baz=1): autoassign(self, locals())
  85. """
  86. #locals = sys._getframe(1).f_locals
  87. #self = locals['self']
  88. for (key, value) in locals.iteritems():
  89. if key == 'self':
  90. continue
  91. setattr(self, key, value)
  92. class Storage(dict):
  93. """
  94. A Storage object is like a dictionary except `obj.foo` can be used
  95. instead of `obj['foo']`. Create one by doing `storage({'a':1})`.
  96. """
  97. def __getattr__(self, key):
  98. if self.has_key(key):
  99. return self[key]
  100. raise AttributeError, repr(key)
  101. def __setattr__(self, key, value):
  102. self[key] = value
  103. def __repr__(self):
  104. return '<Storage ' + dict.__repr__(self) + '>'
  105. storage = Storage
  106. def storify(mapping, *requireds, **defaults):
  107. """
  108. Creates a `storage` object from dictionary `mapping`, raising `KeyError` if
  109. d doesn't have all of the keys in `requireds` and using the default
  110. values for keys found in `defaults`.
  111. For example, `storify({'a':1, 'c':3}, b=2, c=0)` will return the equivalent of
  112. `storage({'a':1, 'b':2, 'c':3})`.
  113. If a `storify` value is a list (e.g. multiple values in a form submission),
  114. `storify` returns the last element of the list, unless the key appears in
  115. `defaults` as a list. Thus:
  116. >>> storify({'a':[1, 2]}).a
  117. 2
  118. >>> storify({'a':[1, 2]}, a=[]).a
  119. [1, 2]
  120. >>> storify({'a':1}, a=[]).a
  121. [1]
  122. >>> storify({}, a=[]).a
  123. []
  124. Similarly, if the value has a `value` attribute, `storify will return _its_
  125. value, unless the key appears in `defaults` as a dictionary.
  126. >>> storify({'a':storage(value=1)}).a
  127. 1
  128. >>> storify({'a':storage(value=1)}, a={}).a
  129. <Storage {'value': 1}>
  130. >>> storify({}, a={}).a
  131. {}
  132. """
  133. def getvalue(x):
  134. if hasattr(x, 'value'):
  135. return x.value
  136. else:
  137. return x
  138. stor = Storage()
  139. for key in requireds + tuple(mapping.keys()):
  140. value = mapping[key]
  141. if isinstance(value, list):
  142. if isinstance(defaults.get(key), list):
  143. value = [getvalue(x) for x in value]
  144. else:
  145. value = value[-1]
  146. if not isinstance(defaults.get(key), dict):
  147. value = getvalue(value)
  148. if isinstance(defaults.get(key), list) and not isinstance(value, list):
  149. value = [value]
  150. setattr(stor, key, value)
  151. for (key, value) in defaults.iteritems():
  152. result = value
  153. if hasattr(stor, key):
  154. result = stor[key]
  155. if value == () and not isinstance(result, tuple):
  156. result = (result,)
  157. setattr(stor, key, result)
  158. return stor
  159. class Memoize:
  160. """
  161. 'Memoizes' a function, caching its return values for each input.
  162. """
  163. def __init__(self, func):
  164. self.func = func
  165. self.cache = {}
  166. def __call__(self, *args, **keywords):
  167. key = (args, tuple(keywords.items()))
  168. if key not in self.cache:
  169. self.cache[key] = self.func(*args, **keywords)
  170. return self.cache[key]
  171. memoize = Memoize
  172. re_compile = memoize(re.compile) #@@ threadsafe?
  173. re_compile.__doc__ = """
  174. A memoized version of re.compile.
  175. """
  176. class _re_subm_proxy:
  177. def __init__(self):
  178. self.match = None
  179. def __call__(self, match):
  180. self.match = match
  181. return ''
  182. def re_subm(pat, repl, string):
  183. """Like re.sub, but returns the replacement _and_ the match object."""
  184. compiled_pat = re_compile(pat)
  185. proxy = _re_subm_proxy()
  186. compiled_pat.sub(proxy.__call__, string)
  187. return compiled_pat.sub(repl, string), proxy.match
  188. def group(seq, size):
  189. """
  190. Returns an iterator over a series of lists of length size from iterable.
  191. For example, `list(group([1,2,3,4], 2))` returns `[[1,2],[3,4]]`.
  192. """
  193. if not hasattr(seq, 'next'):
  194. seq = iter(seq)
  195. while True:
  196. yield [seq.next() for i in xrange(size)]
  197. class IterBetter:
  198. """
  199. Returns an object that can be used as an iterator
  200. but can also be used via __getitem__ (although it
  201. cannot go backwards -- that is, you cannot request
  202. `iterbetter[0]` after requesting `iterbetter[1]`).
  203. """
  204. def __init__(self, iterator):
  205. self.i, self.c = iterator, 0
  206. def __iter__(self):
  207. while 1:
  208. yield self.i.next()
  209. self.c += 1
  210. def __getitem__(self, i):
  211. #todo: slices
  212. if i > self.c:
  213. raise IndexError, "already passed "+str(i)
  214. try:
  215. while i < self.c:
  216. self.i.next()
  217. self.c += 1
  218. # now self.c == i
  219. self.c += 1
  220. return self.i.next()
  221. except StopIteration:
  222. raise IndexError, str(i)
  223. iterbetter = IterBetter
  224. def dictreverse(mapping):
  225. """Takes a dictionary like `{1:2, 3:4}` and returns `{2:1, 4:3}`."""
  226. return dict([(value, key) for (key, value) in mapping.iteritems()])
  227. def dictfind(dictionary, element):
  228. """
  229. Returns a key whose value in `dictionary` is `element`
  230. or, if none exists, None.
  231. """
  232. for (key, value) in dictionary.iteritems():
  233. if element is value:
  234. return key
  235. def dictfindall(dictionary, element):
  236. """
  237. Returns the keys whose values in `dictionary` are `element`
  238. or, if none exists, [].
  239. """
  240. res = []
  241. for (key, value) in dictionary.iteritems():
  242. if element is value:
  243. res.append(key)
  244. return res
  245. def dictincr(dictionary, element):
  246. """
  247. Increments `element` in `dictionary`,
  248. setting it to one if it doesn't exist.
  249. """
  250. dictionary.setdefault(element, 0)
  251. dictionary[element] += 1
  252. return dictionary[element]
  253. def dictadd(dict_a, dict_b):
  254. """
  255. Returns a dictionary consisting of the keys in `a` and `b`.
  256. If they share a key, the value from b is used.
  257. """
  258. result = {}
  259. result.update(dict_a)
  260. result.update(dict_b)
  261. return result
  262. sumdicts = dictadd # deprecated
  263. def listget(lst, ind, default=None):
  264. """Returns `lst[ind]` if it exists, `default` otherwise."""
  265. if len(lst)-1 < ind:
  266. return default
  267. return lst[ind]
  268. def intget(integer, default=None):
  269. """Returns `integer` as an int or `default` if it can't."""
  270. try:
  271. return int(integer)
  272. except (TypeError, ValueError):
  273. return default
  274. def datestr(then, now=None):
  275. """Converts a (UTC) datetime object to a nice string representation."""
  276. def agohence(n, what, divisor=None):
  277. if divisor: n = n // divisor
  278. out = str(abs(n)) + ' ' + what # '2 day'
  279. if abs(n) != 1: out += 's' # '2 days'
  280. out += ' ' # '2 days '
  281. if n < 0:
  282. out += 'from now'
  283. else:
  284. out += 'ago'
  285. return out # '2 days ago'
  286. oneday = 24 * 60 * 60
  287. if not now: now = datetime.datetime.utcnow()
  288. delta = now - then
  289. deltaseconds = int(delta.days * oneday + delta.seconds + delta.microseconds * 1e-06)
  290. deltadays = abs(deltaseconds) // oneday
  291. if deltaseconds < 0: deltadays *= -1 # fix for oddity of floor
  292. if deltadays:
  293. if abs(deltadays) < 4:
  294. return agohence(deltadays, 'day')
  295. out = then.strftime('%B %e') # e.g. 'June 13'
  296. if then.year != now.year or deltadays < 0:
  297. out += ', %s' % then.year
  298. return out
  299. if int(deltaseconds):
  300. if abs(deltaseconds) > (60 * 60):
  301. return agohence(deltaseconds, 'hour', 60 * 60)
  302. elif abs(deltaseconds) > 60:
  303. return agohence(deltaseconds, 'minute', 60)
  304. else:
  305. return agohence(deltaseconds, 'second')
  306. deltamicroseconds = delta.microseconds
  307. if delta.days: deltamicroseconds = int(delta.microseconds - 1e6) # datetime oddity
  308. if abs(deltamicroseconds) > 1000:
  309. return agohence(deltamicroseconds, 'millisecond', 1000)
  310. return agohence(deltamicroseconds, 'microsecond')
  311. def upvars(level=2):
  312. """Guido van Rossum doesn't want you to use this function."""
  313. return dictadd(
  314. sys._getframe(level).f_globals,
  315. sys._getframe(level).f_locals)
  316. class CaptureStdout:
  317. """
  318. Captures everything func prints to stdout and returns it instead.
  319. **WARNING:** Not threadsafe!
  320. """
  321. def __init__(self, func):
  322. self.func = func
  323. def __call__(self, *args, **keywords):
  324. from cStringIO import StringIO
  325. # Not threadsafe!
  326. out = StringIO()
  327. oldstdout = sys.stdout
  328. sys.stdout = out
  329. try:
  330. self.func(*args, **keywords)
  331. finally:
  332. sys.stdout = oldstdout
  333. return out.getvalue()
  334. capturestdout = CaptureStdout
  335. class Profile:
  336. """
  337. Profiles `func` and returns a tuple containing its output
  338. and a string with human-readable profiling information.
  339. """
  340. def __init__(self, func):
  341. self.func = func
  342. def __call__(self, *args): ##, **kw): kw unused
  343. import hotshot, hotshot.stats, tempfile ##, time already imported
  344. temp = tempfile.NamedTemporaryFile()
  345. prof = hotshot.Profile(temp.name)
  346. stime = time.time()
  347. result = prof.runcall(self.func, *args)
  348. stime = time.time() - stime
  349. prof.close()
  350. stats = hotshot.stats.load(temp.name)
  351. stats.strip_dirs()
  352. stats.sort_stats('time', 'calls')
  353. x = '\n\ntook '+ str(stime) + ' seconds\n'
  354. x += capturestdout(stats.print_stats)(40)
  355. x += capturestdout(stats.print_callers)()
  356. return result, x
  357. profile = Profile
  358. def tryall(context, prefix=None):
  359. """
  360. Tries a series of functions and prints their results.
  361. `context` is a dictionary mapping names to values;
  362. the value will only be tried if it's callable.
  363. For example, you might have a file `test/stuff.py`
  364. with a series of functions testing various things in it.
  365. At the bottom, have a line:
  366. if __name__ == "__main__": tryall(globals())
  367. Then you can run `python test/stuff.py` and get the results of
  368. all the tests.
  369. """
  370. context = context.copy() # vars() would update
  371. results = {}
  372. for (key, value) in context.iteritems():
  373. if not hasattr(value, '__call__'):
  374. continue
  375. if prefix and not key.startswith(prefix):
  376. continue
  377. print key + ':',
  378. try:
  379. r = value()
  380. dictincr(results, r)
  381. print r
  382. except:
  383. print 'ERROR'
  384. dictincr(results, 'ERROR')
  385. print ' ' + '\n '.join(traceback.format_exc().split('\n'))
  386. print '-'*40
  387. print 'results:'
  388. for (key, value) in results.iteritems():
  389. print ' '*2, str(key)+':', value
  390. class ThreadedDict:
  391. """
  392. Takes a dictionary that maps threads to objects.
  393. When a thread tries to get or set an attribute or item
  394. of the threadeddict, it passes it on to the object
  395. for that thread in dictionary.
  396. """
  397. def __init__(self, dictionary):
  398. self.__dict__['_ThreadedDict__d'] = dictionary
  399. def __getattr__(self, attr):
  400. return getattr(self.__d[currentThread()], attr)
  401. def __getitem__(self, item):
  402. return self.__d[currentThread()][item]
  403. def __setattr__(self, attr, value):
  404. if attr == '__doc__':
  405. self.__dict__[attr] = value
  406. else:
  407. return setattr(self.__d[currentThread()], attr, value)
  408. def __setitem__(self, item, value):
  409. self.__d[currentThread()][item] = value
  410. def __hash__(self):
  411. return hash(self.__d[currentThread()])
  412. threadeddict = ThreadedDict
  413. ## IP Utilities
  414. def validipaddr(address):
  415. """returns True if `address` is a valid IPv4 address"""
  416. try:
  417. octets = address.split('.')
  418. assert len(octets) == 4
  419. for x in octets:
  420. assert 0 <= int(x) <= 255
  421. except (AssertionError, ValueError):
  422. return False
  423. return True
  424. def validipport(port):
  425. """returns True if `port` is a valid IPv4 port"""
  426. try:
  427. assert 0 <= int(port) <= 65535
  428. except (AssertionError, ValueError):
  429. return False
  430. return True
  431. def validip(ip, defaultaddr="0.0.0.0", defaultport=8080):
  432. """returns `(ip_address, port)` from string `ip_addr_port`"""
  433. addr = defaultaddr
  434. port = defaultport
  435. ip = ip.split(":", 1)
  436. if len(ip) == 1:
  437. if not ip[0]:
  438. pass
  439. elif validipaddr(ip[0]):
  440. addr = ip[0]
  441. elif validipport(ip[0]):
  442. port = int(ip[0])
  443. else:
  444. raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
  445. elif len(ip) == 2:
  446. addr, port = ip
  447. if not validipaddr(addr) and validipport(port):
  448. raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
  449. port = int(port)
  450. else:
  451. raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
  452. return (addr, port)
  453. def validaddr(string_):
  454. """returns either (ip_address, port) or "/path/to/socket" from string_"""
  455. if '/' in string_:
  456. return string_
  457. else:
  458. return validip(string_)
  459. ## URL Utilities
  460. def prefixurl(base=''):
  461. """
  462. Sorry, this function is really difficult to explain.
  463. Maybe some other time.
  464. """
  465. url = ctx.path.lstrip('/')
  466. for i in xrange(url.count('/')):
  467. base += '../'
  468. if not base:
  469. base = './'
  470. return base
  471. def urlquote(x): return urllib.quote(websafe(x).encode('utf-8'))
  472. ## Formatting
  473. try:
  474. from markdown import markdown # http://webpy.org/markdown.py
  475. except ImportError:
  476. pass
  477. r_url = re_compile('(?<!\()(http://(\S+))')
  478. def safemarkdown(text):
  479. """
  480. Converts text to HTML following the rules of Markdown, but blocking any
  481. outside HTML input, so that only the things supported by Markdown
  482. can be used. Also converts raw URLs to links.
  483. (requires [markdown.py](http://webpy.org/markdown.py))
  484. """
  485. if text:
  486. text = text.replace('<', '&lt;')
  487. # TODO: automatically get page title?
  488. text = r_url.sub(r'<\1>', text)
  489. text = markdown(text)
  490. return text
  491. ## Databases
  492. class _ItplError(ValueError):
  493. """String Interpolation Error
  494. from <http://lfw.org/python/Itpl.py>
  495. (cf. below for license)
  496. """
  497. def __init__(self, text, pos):
  498. ValueError.__init__(self)
  499. self.text = text
  500. self.pos = pos
  501. def __str__(self):
  502. return "unfinished expression in %s at char %d" % (
  503. repr(self.text), self.pos)
  504. def _interpolate(format):
  505. """
  506. Takes a format string and returns a list of 2-tuples of the form
  507. (boolean, string) where boolean says whether string should be evaled
  508. or not.
  509. from <http://lfw.org/python/Itpl.py> (public domain, Ka-Ping Yee)
  510. """
  511. def matchorfail(text, pos):
  512. match = tokenprog.match(text, pos)
  513. if match is None:
  514. raise _ItplError(text, pos)
  515. return match, match.end()
  516. namechars = "abcdefghijklmnopqrstuvwxyz" \
  517. "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
  518. chunks = []
  519. pos = 0
  520. while 1:
  521. dollar = format.find("$", pos)
  522. if dollar < 0:
  523. break
  524. nextchar = format[dollar + 1]
  525. if nextchar == "{":
  526. chunks.append((0, format[pos:dollar]))
  527. pos, level = dollar + 2, 1
  528. while level:
  529. match, pos = matchorfail(format, pos)
  530. tstart, tend = match.regs[3]
  531. token = format[tstart:tend]
  532. if token == "{":
  533. level = level + 1
  534. elif token == "}":
  535. level = level - 1
  536. chunks.append((1, format[dollar + 2:pos - 1]))
  537. elif nextchar in namechars:
  538. chunks.append((0, format[pos:dollar]))
  539. match, pos = matchorfail(format, dollar + 1)
  540. while pos < len(format):
  541. if format[pos] == "." and \
  542. pos + 1 < len(format) and format[pos + 1] in namechars:
  543. match, pos = matchorfail(format, pos + 1)
  544. elif format[pos] in "([":
  545. pos, level = pos + 1, 1
  546. while level:
  547. match, pos = matchorfail(format, pos)
  548. tstart, tend = match.regs[3]
  549. token = format[tstart:tend]
  550. if token[0] in "([":
  551. level = level + 1
  552. elif token[0] in ")]":
  553. level = level - 1
  554. else:
  555. break
  556. chunks.append((1, format[dollar + 1:pos]))
  557. else:
  558. chunks.append((0, format[pos:dollar + 1]))
  559. pos = dollar + 1 + (nextchar == "$")
  560. if pos < len(format):
  561. chunks.append((0, format[pos:]))
  562. return chunks
  563. def sqlors(left, lst):
  564. """
  565. `left is a SQL clause like `tablename.arg = `
  566. and `lst` is a list of values. Returns a reparam-style
  567. pair featuring the SQL that ORs together the clause
  568. for each item in the lst.
  569. For example:
  570. web.sqlors('foo =', [1,2,3])
  571. would result in:
  572. foo = 1 OR foo = 2 OR foo = 3
  573. """
  574. if isinstance(lst, iters):
  575. lst = list(lst)
  576. ln = len(lst)
  577. if ln == 0:
  578. return ("2+2=5", [])
  579. if ln == 1:
  580. lst = lst[0]
  581. if isinstance(lst, iters):
  582. return '(' + left + \
  583. (' OR ' + left).join([aparam() for param in lst]) + ")", lst
  584. else:
  585. return left + aparam(), [lst]
  586. class UnknownParamstyle(Exception):
  587. """raised for unsupported db paramstyles
  588. Currently supported: qmark,numeric, format, pyformat
  589. """
  590. pass
  591. def aparam():
  592. """Use in a SQL string to make a spot for a db value."""
  593. style = ctx.db_module.paramstyle
  594. if style == 'qmark':
  595. return '?'
  596. elif style == 'numeric':
  597. return ':1'
  598. elif style in ['format', 'pyformat']:
  599. return '%s'
  600. raise UnknownParamstyle, style
  601. def reparam(string_, dictionary):
  602. """
  603. Takes a string and a dictionary and interpolates the string
  604. using values from the dictionary. Returns a 2-tuple containing
  605. the a string with `aparam()`s in it and a list of the matching values.
  606. You can pass this sort of thing as a clause in any db function.
  607. Otherwise, you can pass a dictionary to the keyword argument `vars`
  608. and the function will call reparam for you.
  609. """
  610. vals = []
  611. result = []
  612. for live, chunk in _interpolate(string_):
  613. if live:
  614. result.append(aparam())
  615. vals.append(eval(chunk, dictionary))
  616. else: result.append(chunk)
  617. return ''.join(result), vals
  618. class UnknownDB(Exception):
  619. """raised for unsupported dbms"""
  620. pass
  621. def connect(dbn, **keywords):
  622. """
  623. Connects to the specified database.
  624. db currently must be "postgres" or "mysql".
  625. If DBUtils is installed, connection pooling will be used.
  626. """
  627. if dbn == "postgres":
  628. try:
  629. import psycopg2 as db
  630. except ImportError:
  631. try:
  632. import psycopg as db
  633. except ImportError:
  634. import pgdb as db
  635. keywords['password'] = keywords['pw']
  636. del keywords['pw']
  637. keywords['database'] = keywords['db']
  638. del keywords['db']
  639. elif dbn == "mysql":
  640. import MySQLdb as db
  641. keywords['passwd'] = keywords['pw']
  642. del keywords['pw']
  643. db.paramstyle = 'pyformat' # it's both, like psycopg
  644. elif dbn == "sqlite":
  645. try: ## try first sqlite3 version
  646. from pysqlite2 import dbapi2 as db
  647. db.paramstyle = 'qmark'
  648. except ImportError: ## else try sqlite2
  649. import sqlite as db
  650. keywords['database'] = keywords['db']
  651. del keywords['db']
  652. else:
  653. raise UnknownDB, dbn
  654. ctx.db_name = dbn
  655. ctx.db_module = db
  656. ctx.db_transaction = False
  657. if _hasPooling:
  658. if 'db' not in globals():
  659. globals()['db'] = PooledDB(dbapi=db, **keywords)
  660. ctx.db = globals()['db'].connection()
  661. else:
  662. ctx.db = db.connect(**keywords)
  663. ctx.dbq_count = 0
  664. if globals().get('db_printing'):
  665. def db_execute(cur, sql_query, d=None):
  666. """executes an sql query"""
  667. def sqlquote(obj):
  668. """converts `obj` to its proper SQL version"""
  669. # because `1 == True and hash(1) == hash(True)`
  670. # we have to do this the hard way...
  671. if obj is None:
  672. return 'NULL'
  673. elif obj is True:
  674. return "'t'"
  675. elif obj is False:
  676. return "'f'"
  677. elif isinstance(obj, datetime.datetime):
  678. return repr(obj.isoformat())
  679. else:
  680. return repr(obj)
  681. ctx.dbq_count += 1
  682. try:
  683. outq = sql_query % tuple(map(sqlquote, d))
  684. except TypeError:
  685. outq = sql_query
  686. print >> debug, str(ctx.dbq_count)+':', outq
  687. a = time.time()
  688. out = cur.execute(sql_query, d)
  689. b = time.time()
  690. print >> debug, '(%s)' % round(b - a, 2)
  691. return out
  692. ctx.db_execute = db_execute
  693. else:
  694. ctx.db_execute = lambda cur, sql_query, d=None: \
  695. cur.execute(sql_query, d)
  696. return ctx.db
  697. def transact():
  698. """Start a transaction."""
  699. # commit everything up to now, so we don't rollback it later
  700. ctx.db.commit()
  701. ctx.db_transaction = True
  702. def commit():
  703. """Commits a transaction."""
  704. ctx.db.commit()
  705. ctx.db_transaction = False
  706. def rollback():
  707. """Rolls back a transaction."""
  708. ctx.db.rollback()
  709. ctx.db_transaction = False
  710. def query(sql_query, vars=None, processed=False):
  711. """
  712. Execute SQL query `sql_query` using dictionary `vars` to interpolate it.
  713. If `processed=True`, `vars` is a `reparam`-style list to use
  714. instead of interpolating.
  715. """
  716. if vars is None:
  717. vars = {}
  718. db_cursor = ctx.db.cursor()
  719. if not processed:
  720. sql_query, vars = reparam(sql_query, vars)
  721. ctx.db_execute(db_cursor, sql_query, vars)
  722. if db_cursor.description:
  723. names = [x[0] for x in db_cursor.description]
  724. def iterwrapper():
  725. row = db_cursor.fetchone()
  726. while row:
  727. yield Storage(dict(zip(names, row)))
  728. row = db_cursor.fetchone()
  729. out = iterbetter(iterwrapper())
  730. out.__len__ = lambda: int(db_cursor.rowcount)
  731. out.list = lambda: [Storage(dict(zip(names, x))) \
  732. for x in db_cursor.fetchall()]
  733. else:
  734. out = db_cursor.rowcount
  735. if not ctx.db_transaction:
  736. ctx.db.commit()
  737. return out
  738. def sqllist(lst):
  739. """
  740. If a list, converts it to a comma-separated string.
  741. Otherwise, returns the string.
  742. """
  743. if isinstance(lst, str):
  744. return lst
  745. else: return ', '.join(lst)
  746. def sqlwhere(dictionary):
  747. """
  748. Converts a `dictionary` to an SQL WHERE clause in
  749. `reparam` format. Thus,
  750. {'cust_id': 2, 'order_id':3}
  751. would result in the equivalent of:
  752. 'cust_id = 2 AND order_id = 3'
  753. but properly quoted.
  754. """
  755. return ' AND '.join([
  756. '%s = %s' % (k, aparam()) for k in dictionary.keys()
  757. ]), dictionary.values()
  758. def select(tables, vars=None, what='*', where=None, order=None, group=None,
  759. limit=None, offset=None):
  760. """
  761. Selects `what` from `tables` with clauses `where`, `order`,
  762. `group`, `limit`, and `offset. Uses vars to interpolate.
  763. Otherwise, each clause can take a reparam-style list.
  764. """
  765. if vars is None:
  766. vars = {}
  767. values = []
  768. qout = ""
  769. for (sql, val) in (
  770. ('SELECT', what),
  771. ('FROM', sqllist(tables)),
  772. ('WHERE', where),
  773. ('GROUP BY', group),
  774. ('ORDER BY', order),
  775. ('LIMIT', limit),
  776. ('OFFSET', offset)):
  777. if isinstance(val, (int, long)):
  778. if sql == 'WHERE':
  779. nquery, nvalue = 'id = '+aparam(), [val]
  780. else:
  781. nquery, nvalue = str(val), ()
  782. elif isinstance(val, (list, tuple)) and len(val) == 2:
  783. nquery, nvalue = val
  784. elif val:
  785. nquery, nvalue = reparam(val, vars)
  786. else:
  787. continue
  788. qout += " " + sql + " " + nquery
  789. values.extend(nvalue)
  790. return query(qout, values, processed=True)
  791. def insert(tablename, seqname=None, **values):
  792. """
  793. Inserts `values` into `tablename`. Returns current sequence ID.
  794. Set `seqname` to the ID if it's not the default, or to `False`
  795. if there isn't one.
  796. """
  797. db_cursor = ctx.db.cursor()
  798. if values:
  799. sql_query, v = "INSERT INTO %s (%s) VALUES (%s)" % (
  800. tablename,
  801. ", ".join(values.keys()),
  802. ', '.join([aparam() for x in values])
  803. ), values.values()
  804. else:
  805. sql_query, v = "INSERT INTO %s DEFAULT VALUES" % tablename, None
  806. if seqname is False:
  807. pass
  808. elif ctx.db_name == "postgres":
  809. if seqname is None:
  810. seqname = tablename + "_id_seq"
  811. sql_query += "; SELECT currval('%s')" % seqname
  812. elif ctx.db_name == "mysql":
  813. ctx.db_execute(db_cursor, sql_query, v)
  814. sql_query = "SELECT last_insert_id()"
  815. v = ()
  816. elif ctx.db_name == "sqlite":
  817. ctx.db_execute(db_cursor, sql_query, v)
  818. # not really the same...
  819. sql_query = "SELECT last_insert_rowid()"
  820. v = ()
  821. ctx.db_execute(db_cursor, sql_query, v)
  822. try:
  823. out = db_cursor.fetchone()[0]
  824. except Exception:
  825. out = None
  826. if not ctx.db_transaction:
  827. ctx.db.commit()
  828. return out
  829. def update(tables, where, vars=None, **values):
  830. """
  831. Update `tables` with clause `where` (interpolated using `vars`)
  832. and setting `values`.
  833. """
  834. if vars is None:
  835. vars = {}
  836. if isinstance(where, (int, long)):
  837. vars = [where]
  838. where = "id = " + aparam()
  839. elif isinstance(where, (list, tuple)) and len(where) == 2:
  840. where, vars = where
  841. else:
  842. where, vars = reparam(where, vars)
  843. db_cursor = ctx.db.cursor()
  844. ctx.db_execute(db_cursor, "UPDATE %s SET %s WHERE %s" % (
  845. sqllist(tables),
  846. ', '.join([k + '=' + aparam() for k in values.keys()]),
  847. where),
  848. values.values() + vars)
  849. if not ctx.db_transaction:
  850. ctx.db.commit()
  851. return db_cursor.rowcount
  852. def delete(table, where, using=None, vars=None):
  853. """
  854. Deletes from `table` with clauses `where` and `using`.
  855. """
  856. if vars is None:
  857. vars = {}
  858. db_cursor = ctx.db.cursor()
  859. if isinstance(where, (int, long)):
  860. vars = [where]
  861. where = "id = " + aparam()
  862. elif isinstance(where, (list, tuple)) and len(where) == 2:
  863. where, vars = where
  864. else:
  865. where, vars = reparam(where, vars)
  866. q = 'DELETE FROM %s WHERE %s' % (table, where)
  867. if using:
  868. q += ' USING ' + sqllist(using)
  869. ctx.db_execute(db_cursor, q, vars)
  870. if not ctx.db_transaction:
  871. ctx.db.commit()
  872. return db_cursor.rowcount
  873. ## Request Handlers
  874. def handle(mapping, fvars=None):
  875. """
  876. Call the appropriate function based on the url to function mapping in `mapping`.
  877. If no module for the function is specified, look up the function in `fvars`. If
  878. `fvars` is empty, using the caller's context.
  879. `mapping` should be a tuple of paired regular expressions with function name
  880. substitutions. `handle` will import modules as necessary.
  881. """
  882. for url, ofno in group(mapping, 2):
  883. if isinstance(ofno, tuple):
  884. ofn, fna = ofno[0], list(ofno[1:])
  885. else:
  886. ofn, fna = ofno, []
  887. fn, result = re_subm('^' + url + '$', ofn, ctx.path)
  888. if result: # it's a match
  889. if fn.split(' ', 1)[0] == "redirect":
  890. url = fn.split(' ', 1)[1]
  891. if ctx.method == "GET":
  892. x = ctx.env.get('QUERY_STRING', '')
  893. if x:
  894. url += '?' + x
  895. return redirect(url)
  896. elif '.' in fn:
  897. x = fn.split('.')
  898. mod, cls = '.'.join(x[:-1]), x[-1]
  899. mod = __import__(mod, globals(), locals(), [""])
  900. cls = getattr(mod, cls)
  901. else:
  902. cls = fn
  903. mod = fvars or upvars()
  904. if isinstance(mod, types.ModuleType):
  905. mod = vars(mod)
  906. try:
  907. cls = mod[cls]
  908. except KeyError:
  909. return notfound()
  910. meth = ctx.method
  911. if meth == "HEAD":
  912. if not hasattr(cls, meth):
  913. meth = "GET"
  914. if not hasattr(cls, meth):
  915. return nomethod(cls)
  916. tocall = getattr(cls(), meth)
  917. args = list(result.groups())
  918. for d in re.findall(r'\\(\d+)', ofn):
  919. args.pop(int(d) - 1)
  920. return tocall(*([urllib.unquote(x) for x in args] + fna))
  921. return notfound()
  922. def autodelegate(prefix=''):
  923. """
  924. Returns a method that takes one argument and calls the method named prefix+arg,
  925. calling `notfound()` if there isn't one. Example:
  926. urls = ('/prefs/(.*)', 'prefs')
  927. class prefs:
  928. GET = autodelegate('GET_')
  929. def GET_password(self): pass
  930. def GET_privacy(self): pass
  931. `GET_password` would get called for `/prefs/password` while `GET_privacy` for
  932. `GET_privacy` gets called for `/prefs/privacy`.
  933. If a user visits `/prefs/password/change` then `GET_password(self, '/change')`
  934. is called.
  935. """
  936. def internal(self, arg):
  937. if '/' in arg:
  938. first, rest = arg.split('/', 1)
  939. func = prefix + first
  940. args = ['/' + rest]
  941. else:
  942. func = prefix + arg
  943. args = []
  944. if hasattr(self, func):
  945. try:
  946. return getattr(self, func)(*args)
  947. except TypeError:
  948. return notfound()
  949. else:
  950. return notfound()
  951. return internal
  952. def background(func):
  953. """A function decorator to run a long-running function as a background thread."""
  954. def internal(*a, **kw):
  955. data() # cache it
  956. ctx = _context[currentThread()]
  957. _context[currentThread()] = storage(ctx.copy())
  958. def newfunc():
  959. _context[currentThread()] = ctx
  960. func(*a, **kw)
  961. t = threading.Thread(target=newfunc)
  962. background.threaddb[id(t)] = t
  963. t.start()
  964. ctx.headers = []
  965. return seeother(changequery(_t=id(t)))
  966. return internal
  967. background.threaddb = {}
  968. def backgrounder(func):
  969. def internal(*a, **kw):
  970. i = input(_method='get')
  971. if '_t' in i:
  972. try:
  973. t = background.threaddb[int(i._t)]
  974. except KeyError:
  975. return notfound()
  976. _context[currentThread()] = _context[t]
  977. return
  978. else:
  979. return func(*a, **kw)
  980. return internal
  981. ## HTTP Functions
  982. def httpdate(date_obj):
  983. """Formats a datetime object for use in HTTP headers."""
  984. return date_obj.strftime("%a, %d %b %Y %H:%M:%S GMT")
  985. def parsehttpdate(string_):
  986. """Parses an HTTP date into a datetime object."""
  987. try:
  988. t = time.strptime(string_, "%a, %d %b %Y %H:%M:%S %Z")
  989. except ValueError:
  990. return None
  991. return datetime.datetime(*t[:6])
  992. def expires(delta):
  993. """
  994. Outputs an `Expires` header for `delta` from now.
  995. `delta` is a `timedelta` object or a number of seconds.
  996. """
  997. try:
  998. datetime
  999. except NameError:
  1000. raise Exception, "requires Python 2.3 or later"
  1001. if isinstance(delta, (int, long)):
  1002. delta = datetime.timedelta(seconds=delta)
  1003. date_obj = datetime.datetime.utcnow() + delta
  1004. header('Expires', httpdate(date_obj))
  1005. def lastmodified(date_obj):
  1006. """Outputs a `Last-Modified` header for `datetime`."""
  1007. header('Last-Modified', httpdate(date_obj))
  1008. def modified(date=None, etag=None):
  1009. n = ctx.env.get('HTTP_IF_NONE_MATCH')
  1010. m = parsehttpdate(ctx.env.get('HTTP_IF_MODIFIED_SINCE', '').split(';')[0])
  1011. validate = False
  1012. if etag:
  1013. raise NotImplementedError, "no etag support yet"
  1014. # should really be a warning
  1015. if date and m:
  1016. # we subtract a second because
  1017. # HTTP dates don't have sub-second precision
  1018. if date-datetime.timedelta(seconds=1) <= m:
  1019. validate = True
  1020. if validate: ctx.status = '304 Not Modified'
  1021. return not validate
  1022. """
  1023. By default, these all return simple error messages that send very short messages
  1024. (like "bad request") to the user. They can and should be overridden
  1025. to return nicer ones.
  1026. """
  1027. def redirect(url, status='301 Moved Permanently'):
  1028. """
  1029. Returns a `status` redirect to the new URL.
  1030. `url` is joined with the base URL so that things like
  1031. `redirect("about") will work properly.
  1032. """
  1033. newloc = urlparse.urljoin(ctx.home + ctx.path, url)
  1034. ctx.status = status
  1035. ctx.output = ''
  1036. header('Content-Type', 'text/html')
  1037. header('Location', newloc)
  1038. # seems to add a three-second delay for some reason:
  1039. # output('<a href="'+ newloc + '">moved permanently</a>')
  1040. def found(url):
  1041. """A `302 Found` redirect."""
  1042. return redirect(url, '302 Found')
  1043. def seeother(url):
  1044. """A `303 See Other` redirect."""
  1045. return redirect(url, '303 See Other')
  1046. def tempredirect(url):
  1047. """A `307 Temporary Redirect` redirect."""
  1048. return redirect(url, '307 Temporary Redirect')
  1049. def badrequest():
  1050. """Return a `400 Bad Request` error."""
  1051. ctx.status = '400 Bad Request'
  1052. header('Content-Type', 'text/html')
  1053. return output('bad request')
  1054. def notfound():
  1055. """Returns a `404 Not Found` error."""
  1056. ctx.status = '404 Not Found'
  1057. header('Content-Type', 'text/html')
  1058. return output('not found')
  1059. def nomethod(cls):
  1060. """Returns a `405 Method Not Allowed` error for `cls`."""
  1061. ctx.status = '405 Method Not Allowed'
  1062. header('Content-Type', 'text/html')
  1063. header('Allow', \
  1064. ', '.join([method for method in \
  1065. ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'] \
  1066. if hasattr(cls, method)]))
  1067. # commented out for the same reason redirect is
  1068. # return output('method not allowed')
  1069. def gone():
  1070. """Returns a `410 Gone` error."""
  1071. ctx.status = '410 Gone'
  1072. header('Content-Type', 'text/html')
  1073. return output("gone")
  1074. def internalerror():
  1075. """Returns a `500 Internal Server` error."""
  1076. ctx.status = "500 Internal Server Error"
  1077. ctx.headers = [('Content-Type', 'text/html')]
  1078. ctx.output = "internal server error"
  1079. # adapted from Django <djangoproject.com>
  1080. # Copyright (c) 2005, the Lawrence Journal-World
  1081. # Used under the modified BSD license:
  1082. # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
  1083. DJANGO_500_PAGE = """#import inspect
  1084. <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
  1085. <html lang="en">
  1086. <head>
  1087. <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  1088. <meta name="robots" content="NONE,NOARCHIVE" />
  1089. <title>$exception_type at $ctx.path</title>
  1090. <style type="text/css">
  1091. html * { padding:0; margin:0; }
  1092. body * { padding:10px 20px; }
  1093. body * * { padding:0; }
  1094. body { font:small sans-serif; }
  1095. body>div { border-bottom:1px solid #ddd; }
  1096. h1 { font-weight:normal; }
  1097. h2 { margin-bottom:.8em; }
  1098. h2 span { font-size:80%; color:#666; font-weight:normal; }
  1099. h3 { margin:1em 0 .5em 0; }
  1100. h4 { margin:0 0 .5em 0; font-weight: normal; }
  1101. table {
  1102. border:1px solid #ccc; border-collapse: collapse; background:white; }
  1103. tbody td, tbody th { vertical-align:top; padding:2px 3px; }
  1104. thead th {
  1105. padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
  1106. font-weight:normal; font-size:11px; border:1px solid #ddd; }
  1107. tbody th { text-align:right; color:#666; padding-right:.5em; }
  1108. table.vars { margin:5px 0 2px 40px; }
  1109. table.vars td, table.req td { font-family:monospace; }
  1110. table td.code { width:100%;}
  1111. table td.code div { overflow:hidden; }
  1112. table.source th { color:#666; }
  1113. table.source td {
  1114. font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
  1115. ul.traceback { list-style-type:none; }
  1116. ul.traceback li.frame { margin-bottom:1em; }
  1117. div.context { margin: 10px 0; }
  1118. div.context ol {
  1119. padding-left:30px; margin:0 10px; list-style-position: inside; }
  1120. div.context ol li {
  1121. font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
  1122. div.context ol.context-line li { color:black; background-color:#ccc; }
  1123. div.context ol.context-line li span { float: right; }
  1124. div.commands { margin-left: 40px; }
  1125. div.commands a { color:black; text-decoration:none; }
  1126. #summary { background: #ffc; }
  1127. #summary h2 { font-weight: normal; color: #666; }
  1128. #explanation { background:#eee; }
  1129. #template, #template-not-exist { background:#f6f6f6; }
  1130. #template-not-exist ul { margin: 0 0 0 20px; }
  1131. #traceback { background:#eee; }
  1132. #requestinfo { background:#f6f6f6; padding-left:120px; }
  1133. #summary table { border:none; background:transparent; }
  1134. #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
  1135. #requestinfo h3 { margin-bottom:-1em; }
  1136. .error { background: #ffc; }
  1137. .specific { color:#cc3300; font-weight:bold; }
  1138. </style>
  1139. <script type="text/javascript">
  1140. //<!--
  1141. function getElementsByClassName(oElm, strTagName, strClassName){
  1142. // Written by Jonathan Snook, http://www.snook.ca/jon;
  1143. // Add-ons by Robert Nyman, http://www.robertnyman.com
  1144. var arrElements = (strTagName == "*" && document.all)? document.all :
  1145. oElm.getElementsByTagName(strTagName);
  1146. var arrReturnElements = new Array();
  1147. strClassName = strClassName.replace(/\-/g, "\\-");
  1148. var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$)");
  1149. var oElement;
  1150. for(var i=0; i<arrElements.length; i++){
  1151. oElement = arrElements[i];
  1152. if(oRegExp.test(oElement.className)){
  1153. arrReturnElements.push(oElement);
  1154. }
  1155. }
  1156. return (arrReturnElements)
  1157. }
  1158. function hideAll(elems) {
  1159. for (var e = 0; e < elems.length; e++) {
  1160. elems[e].style.display = 'none';
  1161. }
  1162. }
  1163. window.onload = function() {
  1164. hideAll(getElementsByClassName(document, 'table', 'vars'));
  1165. hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
  1166. hideAll(getElementsByClassName(document, 'ol', 'post-context'));
  1167. }
  1168. function toggle() {
  1169. for (var i = 0; i < arguments.length; i++) {
  1170. var e = document.getElementById(arguments[i]);
  1171. if (e) {
  1172. e.style.display = e.style.display == 'none' ? 'block' : 'none';
  1173. }
  1174. }
  1175. return false;
  1176. }
  1177. function varToggle(link, id) {
  1178. toggle('v' + id);
  1179. var s = link.getElementsByTagName('span')[0];
  1180. var uarr = String.fromCharCode(0x25b6);
  1181. var darr = String.fromCharCode(0x25bc);
  1182. s.innerHTML = s.innerHTML == uarr ? darr : uarr;
  1183. return false;
  1184. }
  1185. //-->
  1186. </script>
  1187. </head>
  1188. <body>
  1189. <div id="summary">
  1190. <h1>$exception_type at $ctx.path</h1>
  1191. <h2>$exception_value</h2>
  1192. <table><tr>
  1193. <th>Python</th>
  1194. <td>$lastframe.filename in $lastframe.function, line $lastframe.lineno</td>
  1195. </tr><tr>
  1196. <th>Web</th>
  1197. <td>$ctx.method $ctx.home$ctx.path</td>
  1198. </tr></table>
  1199. </div>
  1200. <div id="traceback">
  1201. <h2>Traceback <span>(innermost first)</span></h2>
  1202. <ul class="traceback">
  1203. #for frame in $frames
  1204. <li class="frame">
  1205. <code>$frame.filename</code> in <code>$frame.function</code>
  1206. #if $frame.context_line
  1207. <div class="context" id="c$frame.id">
  1208. #if $frame.pre_context
  1209. <ol start="$frame.pre_context_lineno" class="pre-context" id="pre$frame.id">#for line in $frame.pre_context#<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>#end for#</ol>
  1210. #end if
  1211. <ol start="$frame.lineno" class="context-line"><li onclick="toggle('pre$frame.id', 'post$frame.id')">$frame.context_line <span>...</span></li></ol>
  1212. #if $frame.post_context
  1213. <ol start='$(frame.lineno+1)' class="post-context" id="post$frame.id">#for line in $frame.post_context#<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>#end for#</ol>
  1214. #end if
  1215. </div>
  1216. #end if
  1217. #if $frame.vars
  1218. <div class="commands">
  1219. <a href='#' onclick="return varToggle(this, '$frame.id')"><span>&#x25b6;</span> Local vars</a>## $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame))
  1220. </div>
  1221. <table class="vars" id="v$frame.id">
  1222. <thead>
  1223. <tr>
  1224. <th>Variable</th>
  1225. <th>Value</th>
  1226. </tr>
  1227. </thead>
  1228. <tbody>
  1229. #set frameitems = $frame.vars
  1230. #silent frameitems.sort(lambda x,y: cmp(x[0], y[0]))
  1231. #for (key, val) in frameitems
  1232. <tr>
  1233. <td>$key</td>
  1234. <td class="code"><div>$prettify(val)</div></td>
  1235. </tr>
  1236. #end for
  1237. </tbody>
  1238. </table>
  1239. #end if
  1240. </li>
  1241. #end for
  1242. </ul>
  1243. </div>
  1244. <div id="requestinfo">
  1245. #if $context_.output or $context_.headers
  1246. <h2>Response so far</h2>
  1247. <h3>HEADERS</h3>
  1248. #if $ctx.headers
  1249. <p class="req"><code>
  1250. #for (k, v) in $context_.headers
  1251. $k: $v<br />
  1252. #end for
  1253. </code></p>
  1254. #else
  1255. <p>No headers.</p>
  1256. #end if
  1257. <h3>BODY</h3>
  1258. <p class="req" style="padding-bottom: 2em"><code>
  1259. $context_.output
  1260. </code></p>
  1261. #end if
  1262. <h2>Request information</h2>
  1263. <h3>INPUT</h3>
  1264. #if $input_
  1265. <table class="req">
  1266. <thead>
  1267. <tr>
  1268. <th>Variable</th>
  1269. <th>Value</th>
  1270. </tr>
  1271. </thead>
  1272. <tbody>
  1273. #set myitems = $input_.items()
  1274. #silent myitems.sort(lambda x,y: cmp(x[0], y[0]))
  1275. #for (key, val) in myitems
  1276. <tr>
  1277. <td>$key</td>
  1278. <td class="code"><div>$val</div></td>
  1279. </tr>
  1280. #end for
  1281. </tbody>
  1282. </table>
  1283. #else
  1284. <p>No input data.</p>
  1285. #end if
  1286. <h3 id="cookie-info">COOKIES</h3>
  1287. #if $cookies_
  1288. <table class="req">
  1289. <thead>
  1290. <tr>
  1291. <th>Variable</th>
  1292. <th>Value</th>
  1293. </tr>
  1294. </thead>
  1295. <tbody>
  1296. #for (key, val) in $cookies_.items()
  1297. <tr>
  1298. <td>$key</td>
  1299. <td class="code"><div>$val</div></td>
  1300. </tr>
  1301. #end for
  1302. </tbody>
  1303. </table>
  1304. #else
  1305. <p>No cookie data</p>
  1306. #end if
  1307. <h3 id="meta-info">META</h3>
  1308. <table class="req">
  1309. <thead>
  1310. <tr>
  1311. <th>Variable</th>
  1312. <th>Value</th>
  1313. </tr>
  1314. </thead>
  1315. <tbody>
  1316. #set myitems = $context_.items()
  1317. #silent myitems.sort(lambda x,y: cmp(x[0], y[0]))
  1318. #for (key, val) in $myitems
  1319. #if not $key.startswith('_') and $key not in ['env', 'output', 'headers', 'environ', 'status', 'db_execute']
  1320. <tr>
  1321. <td>$key</td>
  1322. <td class="code"><div>$prettify($val)</div></td>
  1323. </tr>
  1324. #end if
  1325. #end for
  1326. </tbody>
  1327. </table>
  1328. <h3 id="meta-info">ENVIRONMENT</h3>
  1329. <table class="req">
  1330. <thead>
  1331. <tr>
  1332. <th>Variable</th>
  1333. <th>Value</th>
  1334. </tr>
  1335. </thead>
  1336. <tbody>
  1337. #set myitems = $context_.env.items()
  1338. #silent myitems.sort(lambda x,y: cmp(x[0], y[0]))
  1339. #for (key, val) in $myitems
  1340. <tr>
  1341. <td>$key</td>
  1342. <td class="code"><div>$prettify($val)</div></td>
  1343. </tr>
  1344. #end for
  1345. </tbody>
  1346. </table>
  1347. </div>
  1348. <div id="explanation">
  1349. <p>
  1350. You're seeing this error because you have <code>web.internalerror</code>
  1351. set to <code>web.debugerror</code>. Change that if you want a different one.
  1352. </p>
  1353. </div>
  1354. </body>
  1355. </html>"""
  1356. def djangoerror():
  1357. def _get_lines_from_file(filename, lineno, context_lines):
  1358. """
  1359. Returns context_lines before and after lineno from file.
  1360. Returns (pre_context_lineno, pre_context, context_line, post_context).
  1361. """
  1362. try:
  1363. source = open(filename).readlines()
  1364. lower_bound = max(0, lineno - context_lines)
  1365. upper_bound = lineno + context_lines
  1366. pre_context = \
  1367. [line.strip('\n') for line in source[lower_bound:lineno]]
  1368. context_line = source[lineno].strip('\n')
  1369. post_context = \
  1370. [line.strip('\n') for line in source[lineno + 1:upper_bound]]
  1371. return lower_bound, pre_context, context_line, post_context
  1372. except (OSError, IOError):
  1373. return None, [], None, []
  1374. exception_type, exception_value, tback = sys.exc_info()
  1375. frames = []
  1376. while tback is not None:
  1377. filename = tback.tb_frame.f_code.co_filename
  1378. function = tback.tb_frame.f_code.co_name
  1379. lineno = tback.tb_lineno - 1
  1380. pre_context_lineno, pre_context, context_line, post_context = \
  1381. _get_lines_from_file(filename, lineno, 7)
  1382. frames.append({
  1383. 'tback': tback,
  1384. 'filename': filename,
  1385. 'function': function,
  1386. 'lineno': lineno,
  1387. 'vars': tback.tb_frame.f_locals.items(),
  1388. 'id': id(tback),
  1389. 'pre_context': pre_context,
  1390. 'context_line': context_line,
  1391. 'post_context': post_context,
  1392. 'pre_context_lineno': pre_context_lineno,
  1393. })
  1394. tback = tback.tb_next
  1395. lastframe = frames[-1]
  1396. frames.reverse()
  1397. urljoin = urlparse.urljoin
  1398. input_ = input()
  1399. cookies_ = cookies()
  1400. context_ = ctx
  1401. def prettify(x):
  1402. try:
  1403. out = pprint.pformat(x)
  1404. except Exception, e:
  1405. out = '[could not display: <' + e.__class__.__name__ + \
  1406. ': '+str(e)+'>]'
  1407. return out
  1408. return render(DJANGO_500_PAGE, asTemplate=True, isString=True)
  1409. def debugerror():
  1410. """
  1411. A replacement for `internalerror` that presents a nice page with lots
  1412. of debug information for the programmer.
  1413. (Based on the beautiful 500 page from [Django](http://djangoproject.com/),
  1414. designed by [Wilson Miner](http://wilsonminer.com/).)
  1415. Requires [Cheetah](http://cheetahtemplate.org/).
  1416. """
  1417. # need to do django first, so it can get the old stuff
  1418. if _hasTemplating:
  1419. out = str(djangoerror())
  1420. else:
  1421. # Cheetah isn't installed
  1422. out = """<p>You've set web.py to use the fancier debugerror error
  1423. messages, but these messages require you install the Cheetah template
  1424. system. For more information, see
  1425. <a href="http://webpy.org/">the web.py website</a>.</p>
  1426. <p>In the meantime, here's a plain old error message:</p>
  1427. <pre>%s</pre>
  1428. <p>(If it says something about 'Compiler', then it's probably
  1429. because you're trying to use templates and you haven't
  1430. installed Cheetah. See above.)</p>
  1431. """ % htmlquote(traceback.format_exc())
  1432. ctx.status = "500 Internal Server Error"
  1433. ctx.headers = [('Content-Type', 'text/html')]
  1434. ctx.output = out
  1435. ## Rendering
  1436. r_include = re_compile(r'(?!\\)#include \"(.*?)\"($|#)', re.M)
  1437. def __compiletemplate(template, base=None, isString=False):
  1438. if isString:
  1439. text = template
  1440. else:
  1441. text = open('templates/'+template).read()
  1442. # implement #include at compile-time
  1443. def do_include(match):
  1444. text = open('templates/'+match.groups()[0]).read()
  1445. return text
  1446. while r_include.findall(text):
  1447. text = r_include.sub(do_include, text)
  1448. execspace = _compiletemplate.bases.copy()
  1449. tmpl_compiler = Compiler(source=text, mainClassName='GenTemplate')
  1450. tmpl_compiler.addImportedVarNames(execspace.keys())
  1451. exec str(tmpl_compiler) in execspace
  1452. if base:
  1453. _compiletemplate.bases[base] = execspace['GenTemplate']
  1454. return execspace['GenTemplate']
  1455. _compiletemplate = memoize(__compiletemplate)
  1456. _compiletemplate.bases = {}
  1457. def htmlquote(text):
  1458. """Encodes `text` for raw use in HTML."""
  1459. text = text.replace("&", "&amp;") # Must be done first!
  1460. text = text.replace("<", "&lt;")
  1461. text = text.replace(">", "&gt;")
  1462. text = text.replace("'", "&#39;")
  1463. text = text.replace('"', "&quot;")
  1464. return text
  1465. def websafe(val):
  1466. """
  1467. Converts `val` so that it's safe for use in HTML.
  1468. HTML metacharacters are encoded,
  1469. None becomes the empty string, and
  1470. unicode is converted to UTF-8.
  1471. """
  1472. if val is None: return ''
  1473. if not isinstance(val, unicode): val = str(val)
  1474. return htmlquote(val)
  1475. if _hasTemplating:
  1476. class WebSafe(Filter):
  1477. def filter(self, val, **keywords):
  1478. return websafe(val)
  1479. def render(template, terms=None, asTemplate=False, base=None,
  1480. isString=False):
  1481. """
  1482. Renders a template, caching where it can.
  1483. `template` is the name of a file containing the a template in
  1484. the `templates/` folder, unless `isString`, in which case it's the
  1485. template itself.
  1486. `terms` is a dictionary used to fill the template. If it's None, then
  1487. the caller's local variables are used instead, plus context, if it's not
  1488. already set, is set to `context`.
  1489. If asTemplate is False, it `output`s the template directly. Otherwise,
  1490. it returns the template object.
  1491. If the template is a potential base template (that is, something other templates)
  1492. can extend, then base should be a string with the name of the template. The
  1493. template will be cached and made available for future calls to `render`.
  1494. Requires [Cheetah](http://cheetahtemplate.org/).
  1495. """
  1496. # terms=['var1', 'var2'] means grab those variables
  1497. if isinstance(terms, list):
  1498. new = {}
  1499. old = upvars()
  1500. for k in terms:
  1501. new[k] = old[k]
  1502. terms = new
  1503. # default: grab all locals
  1504. elif terms is None:
  1505. terms = {'context': context, 'ctx':ctx}
  1506. terms.update(sys._getframe(1).f_locals)
  1507. # terms=d means use d as the searchList
  1508. if not isinstance(terms, tuple):
  1509. terms = (terms,)
  1510. if not isString and template.endswith('.html'):
  1511. header('Content-Type','text/html; charset=utf-8', unique=True)
  1512. compiled_tmpl = _compiletemplate(template, base=base, isString=isString)
  1513. compiled_tmpl = compiled_tmpl(searchList=terms, filter=WebSafe)
  1514. if asTemplate:
  1515. return compiled_tmpl
  1516. else:
  1517. return output(str(compiled_tmpl))
  1518. ## Input Forms
  1519. def input(*requireds, **defaults):
  1520. """
  1521. Returns a `storage` object with the GET and POST arguments.
  1522. See `storify` for how `requireds` and `defaults` work.
  1523. """
  1524. from cStringIO import StringIO
  1525. def dictify(fs): return dict([(k, fs[k]) for k in fs.keys()])
  1526. _method = defaults.pop('_method', 'both')
  1527. e = ctx.env.copy()
  1528. out = {}
  1529. if _method.lower() in ['both', 'post']:
  1530. a = {}
  1531. if e['REQUEST_METHOD'] == 'POST':
  1532. a = cgi.FieldStorage(fp = StringIO(data()), environ=e,
  1533. keep_blank_values=1)
  1534. a = dictify(a)
  1535. out = dictadd(out, a)
  1536. if _method.lower() in ['both', 'get']:
  1537. e['REQUEST_METHOD'] = 'GET'
  1538. a = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
  1539. out = dictadd(out, a)
  1540. try:
  1541. return storify(out, *requireds, **defaults)
  1542. except KeyError:
  1543. badrequest()
  1544. raise StopIteration
  1545. def data():
  1546. """Returns the data sent with the request."""
  1547. if 'data' not in ctx:
  1548. cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
  1549. ctx.data = ctx.env['wsgi.input'].read(cl)
  1550. return ctx.data
  1551. def changequery(**kw):
  1552. """
  1553. Imagine you're at `/foo?a=1&b=2`. Then `changequery(a=3)` will return
  1554. `/foo?a=3&b=2` -- the same URL but with the arguments you requested
  1555. changed.
  1556. """
  1557. query = input(_method='get')
  1558. for k, v in kw.iteritems():
  1559. if v is None:
  1560. query.pop(k, None)
  1561. else:
  1562. query[k] = v
  1563. out = ctx.path
  1564. if query:
  1565. out += '?' + urllib.urlencode(query)
  1566. return out
  1567. ## Cookies
  1568. def setcookie(name, value, expires="", domain=None):
  1569. """Sets a cookie."""
  1570. if expires < 0:
  1571. expires = -1000000000
  1572. kargs = {'expires': expires, 'path':'/'}
  1573. if domain:
  1574. kargs['domain'] = domain
  1575. # @@ should we limit cookies to a different path?
  1576. cookie = Cookie.SimpleCookie()
  1577. cookie[name] = value
  1578. for key, val in kargs.iteritems():
  1579. cookie[name][key] = val
  1580. header('Set-Cookie', cookie.items()[0][1].OutputString())
  1581. def cookies(*requireds, **defaults):
  1582. """
  1583. Returns a `storage` object with all the cookies in it.
  1584. See `storify` for how `requireds` and `defaults` work.
  1585. """
  1586. cookie = Cookie.SimpleCookie()
  1587. cookie.load(ctx.env.get('HTTP_COOKIE', ''))
  1588. try:
  1589. return storify(cookie, *requireds, **defaults)
  1590. except KeyError:
  1591. badrequest()
  1592. raise StopIteration
  1593. ## WSGI Sugar
  1594. def header(hdr, value, unique=False):
  1595. """
  1596. Adds the header `hdr: value` with the response.
  1597. If `unique` is True and a header with that name already exists,
  1598. it doesn't add a new one. If `unique` is None and a header with
  1599. that name already exists, it replaces it with this one.
  1600. """
  1601. if unique is True:
  1602. for h, v in ctx.headers:
  1603. if h == hdr: return
  1604. elif unique is None:
  1605. ctx.headers = [h for h in ctx.headers if h[0] != hdr]
  1606. ctx.headers.append((hdr, value))
  1607. def output(string_):
  1608. """Appends `string_` to the response."""
  1609. if isinstance(string_, unicode): string_ = string_.encode('utf8')
  1610. if ctx.get('flush'):
  1611. ctx._write(string_)
  1612. else:
  1613. ctx.output += str(string_)
  1614. def flush():
  1615. ctx.flush = True
  1616. return flush
  1617. def write(cgi_response):
  1618. """
  1619. Converts a standard CGI-style string response into `header` and
  1620. `output` calls.
  1621. """
  1622. cgi_response = str(cgi_response)
  1623. cgi_response.replace('\r\n', '\n')
  1624. head, body = cgi_response.split('\n\n', 1)
  1625. lines = head.split('\n')
  1626. for line in lines:
  1627. if line.isspace():
  1628. continue
  1629. hdr, value = line.split(":", 1)
  1630. value = value.strip()
  1631. if hdr.lower() == "status":
  1632. ctx.status = value
  1633. else:
  1634. header(hdr, value)
  1635. output(body)
  1636. def webpyfunc(inp, fvars=None, autoreload=False):
  1637. """If `inp` is a url mapping, returns a function that calls handle."""
  1638. if not fvars:
  1639. fvars = upvars()
  1640. if not hasattr(inp, '__call__'):
  1641. if autoreload:
  1642. # black magic to make autoreload work:
  1643. mod = \
  1644. __import__(
  1645. fvars['__file__'].split(os.path.sep).pop().split('.')[0])
  1646. #@@probably should replace this with some inspect magic
  1647. name = dictfind(fvars, inp)
  1648. func = lambda: handle(getattr(mod, name), mod)
  1649. else:
  1650. func = lambda: handle(inp, fvars)
  1651. else:
  1652. func = inp
  1653. return func
  1654. def wsgifunc(func, *middleware):
  1655. """Returns a WSGI-compatible function from a webpy-function."""
  1656. middleware = list(middleware)
  1657. if reloader in middleware:
  1658. relr = reloader(None)
  1659. relrcheck = relr.check
  1660. middleware.remove(reloader)
  1661. else:
  1662. relr = None
  1663. relrcheck = lambda: None
  1664. def wsgifunc(env, start_resp):
  1665. _load(env)
  1666. relrcheck()
  1667. try:
  1668. result = func()
  1669. except StopIteration:
  1670. result = None
  1671. is_generator = result and hasattr(result, 'next')
  1672. if is_generator:
  1673. # wsgi requires the headers first
  1674. # so we need to do an iteration
  1675. # and save the result for later
  1676. try:
  1677. firstchunk = result.next()
  1678. except StopIteration:
  1679. firstchunk = ''
  1680. status, headers, output = ctx.status, ctx.headers, ctx.output
  1681. ctx._write = start_resp(status, headers)
  1682. # and now, the fun:
  1683. def cleanup():
  1684. # we insert this little generator
  1685. # at the end of our itertools.chain
  1686. # so that it unloads the request
  1687. # when everything else is done
  1688. yield '' # force it to be a generator
  1689. _unload()
  1690. # result is the output of calling the webpy function
  1691. # it could be a generator...
  1692. if is_generator:
  1693. if firstchunk is flush:
  1694. # oh, it's just our special flush mode
  1695. # ctx._write is set up, so just continue execution
  1696. try:
  1697. result.next()
  1698. except StopIteration:
  1699. pass
  1700. _unload()
  1701. return []
  1702. else:
  1703. return itertools.chain([firstchunk], result, cleanup())
  1704. # ... but it's usually just None
  1705. #
  1706. # output is the stuff in ctx.output
  1707. # it's usually a string...
  1708. if isinstance(output, str): #@@ other stringlikes?
  1709. _unload()
  1710. return [output]
  1711. # it could be a generator...
  1712. elif hasattr(output, 'next'):
  1713. return itertools.chain(output, cleanup())
  1714. else:
  1715. _unload()
  1716. raise Exception, "Invalid web.ctx.output"
  1717. for mw_func in middleware:
  1718. wsgifunc = mw_func(wsgifunc)
  1719. if relr:
  1720. relr.func = wsgifunc
  1721. return wsgifunc
  1722. return wsgifunc
  1723. def run(inp, *middleware):
  1724. """
  1725. Starts handling requests. If called in a CGI or FastCGI context, it will follow
  1726. that protocol. If called from the command line, it will start an HTTP
  1727. server on the port named in the first command line argument, or, if there
  1728. is no argument, on port 8080.
  1729. `input` is a callable, then it's called with no arguments.
  1730. Otherwise, it's a `mapping` object to be passed to `handle(...)`.
  1731. **Caveat:** So that `reloader` will work correctly, input has to be a variable,
  1732. it can't be a tuple passed in directly.
  1733. `middleware` is a list of WSGI middleware which is applied to the resulting WSGI
  1734. function.
  1735. """
  1736. autoreload = reloader in middleware
  1737. fvars = upvars()
  1738. return runwsgi(wsgifunc(webpyfunc(inp, fvars, autoreload), *middleware))
  1739. def runwsgi(func):
  1740. """
  1741. Runs a WSGI-compatible function using FCGI, SCGI, or a simple web server,
  1742. as appropriate.
  1743. """
  1744. #@@ improve detection
  1745. if os.environ.has_key('SERVER_SOFTWARE'): # cgi
  1746. os.environ['FCGI_FORCE_CGI'] = 'Y'
  1747. if (os.environ.has_key('PHP_FCGI_CHILDREN') #lighttpd fastcgi
  1748. or os.environ.has_key('SERVER_SOFTWARE')
  1749. or 'fcgi' in sys.argv or 'fastcgi' in sys.argv):
  1750. return runfcgi(func)
  1751. if 'scgi' in sys.argv:
  1752. return runscgi(func)
  1753. # command line:
  1754. return runsimple(func, validip(listget(sys.argv, 1, '')))
  1755. def runsimple(func, server_address=("0.0.0.0", 8080)):
  1756. """
  1757. Runs a simple HTTP server hosting WSGI app `func`. The directory `static/`
  1758. is hosted statically.
  1759. Based on [WsgiServer][ws] from [Colin Stewart][cs].
  1760. [ws]: http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html
  1761. [cs]: http://www.owlfish.com/
  1762. """
  1763. # Copyright (c) 2004 Colin Stewart (http://www.owlfish.com/)
  1764. # Modified somewhat for simplicity
  1765. # Used under the modified BSD license:
  1766. # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
  1767. import SimpleHTTPServer, SocketServer, BaseHTTPServer, urlparse
  1768. import socket, errno
  1769. import traceback
  1770. class WSGIHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
  1771. def run_wsgi_app(self):
  1772. protocol, host, path, parameters, query, fragment = \
  1773. urlparse.urlparse('http://dummyhost%s' % self.path)
  1774. # we only use path, query
  1775. env = {'wsgi.version': (1, 0)
  1776. ,'wsgi.url_scheme': 'http'
  1777. ,'wsgi.input': self.rfile
  1778. ,'wsgi.errors': sys.stderr
  1779. ,'wsgi.multithread': 1
  1780. ,'wsgi.multiprocess': 0
  1781. ,'wsgi.run_once': 0
  1782. ,'REQUEST_METHOD': self.command
  1783. ,'REQUEST_URI': self.path
  1784. ,'PATH_INFO': path
  1785. ,'QUERY_STRING': query
  1786. ,'CONTENT_TYPE': self.headers.get('Content-Type', '')
  1787. ,'CONTENT_LENGTH': self.headers.get('Content-Length', '')
  1788. ,'REMOTE_ADDR': self.client_address[0]
  1789. ,'SERVER_NAME': self.server.server_address[0]
  1790. ,'SERVER_PORT': str(self.server.server_address[1])
  1791. ,'SERVER_PROTOCOL': self.request_version
  1792. }
  1793. for http_header, http_value in self.headers.items():
  1794. env ['HTTP_%s' % http_header.replace('-', '_').upper()] = \
  1795. http_value
  1796. # Setup the state
  1797. self.wsgi_sent_headers = 0
  1798. self.wsgi_headers = []
  1799. try:
  1800. # We have there environment, now invoke the application
  1801. result = self.server.app(env, self.wsgi_start_response)
  1802. try:
  1803. try:
  1804. for data in result:
  1805. if data:
  1806. self.wsgi_write_data(data)
  1807. finally:
  1808. if hasattr(result, 'close'):
  1809. result.close()
  1810. except socket.error, socket_err:
  1811. # Catch common network errors and suppress them
  1812. if (socket_err.args[0] in \
  1813. (errno.ECONNABORTED, errno.EPIPE)):
  1814. return
  1815. except socket.timeout, socket_timeout:
  1816. return
  1817. except:
  1818. print >> debug, traceback.format_exc(),
  1819. internalerror()
  1820. if not self.wsgi_sent_headers:
  1821. self.wsgi_start_response(ctx.status, ctx.headers)
  1822. self.wsgi_write_data(ctx.output)
  1823. if (not self.wsgi_sent_headers):
  1824. # We must write out something!
  1825. self.wsgi_write_data(" ")
  1826. return
  1827. do_POST = run_wsgi_app
  1828. do_PUT = run_wsgi_app
  1829. do_DELETE = run_wsgi_app
  1830. def do_GET(self):
  1831. if self.path.startswith('/static/'):
  1832. SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
  1833. else:
  1834. self.run_wsgi_app()
  1835. def wsgi_start_response(self, response_status, response_headers,
  1836. exc_info=None):
  1837. if (self.wsgi_sent_headers):
  1838. raise Exception \
  1839. ("Headers already sent and start_response called again!")
  1840. # Should really take a copy to avoid changes in the application....
  1841. self.wsgi_headers = (response_status, response_headers)
  1842. return self.wsgi_write_data
  1843. def wsgi_write_data(self, data):
  1844. if (not self.wsgi_sent_headers):
  1845. status, headers = self.wsgi_headers
  1846. # Need to send header prior to data
  1847. status_code = status [:status.find(' ')]
  1848. status_msg = status [status.find(' ') + 1:]
  1849. self.send_response(int(status_code), status_msg)
  1850. for header, value in headers:
  1851. self.send_header(header, value)
  1852. self.end_headers()
  1853. self.wsgi_sent_headers = 1
  1854. # Send the data
  1855. self.wfile.write(data)
  1856. class WSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
  1857. def __init__(self, func, server_address):
  1858. BaseHTTPServer.HTTPServer.__init__(self,
  1859. server_address,
  1860. WSGIHandler)
  1861. self.app = func
  1862. self.serverShuttingDown = 0
  1863. print "Launching server: http://%s:%d/" % server_address
  1864. WSGIServer(func, server_address).serve_forever()
  1865. def makeserver(wsgi_server):
  1866. """Updates a flup-style WSGIServer with web.py-style error support."""
  1867. class MyServer(wsgi_server):
  1868. def error(self, req):
  1869. w = req.stdout.write
  1870. internalerror()
  1871. w('Status: ' + ctx.status + '\r\n')
  1872. for (h, v) in ctx.headers:
  1873. w(h + ': ' + v + '\r\n')
  1874. w('\r\n' + ctx.output)
  1875. return MyServer
  1876. def runfcgi(func):
  1877. """Runs a WSGI-function with a FastCGI server."""
  1878. from flup.server.fcgi import WSGIServer
  1879. if len(sys.argv) > 2: # progname, scgi
  1880. args = sys.argv[:]
  1881. if 'fastcgi' in args: args.remove('fastcgi')
  1882. elif 'fcgi' in args: args.remove('fcgi')
  1883. hostport = validaddr(args[1])
  1884. elif len(sys.argv) > 1:
  1885. hostport = ('localhost', 8000)
  1886. else:
  1887. hostport = None
  1888. return makeserver(WSGIServer)(func, multiplexed=True, bindAddress=hostport).run()
  1889. def runscgi(func):
  1890. """Runs a WSGI-function with an SCGI server."""
  1891. from flup.server.scgi import WSGIServer
  1892. my_server = makeserver(WSGIServer)
  1893. if len(sys.argv) > 2: # progname, scgi
  1894. args = sys.argv[:]
  1895. args.remove('scgi')
  1896. hostport = validaddr(args[1])
  1897. else:
  1898. hostport = ('localhost', 4000)
  1899. return my_server(func, bindAddress=hostport).run()
  1900. ## Debugging
  1901. def debug(*args):
  1902. """
  1903. Prints a prettyprinted version of `args` to stderr.
  1904. """
  1905. try:
  1906. out = ctx.environ['wsgi.errors']
  1907. except:
  1908. out = sys.stderr
  1909. for arg in args:
  1910. print >> out, pprint.pformat(arg)
  1911. return ''
  1912. def debugwrite(x):
  1913. """writes debug data to error stream"""
  1914. try:
  1915. out = ctx.environ['wsgi.errors']
  1916. except:
  1917. out = sys.stderr
  1918. out.write(x)
  1919. debug.write = debugwrite
  1920. class Reloader:
  1921. """
  1922. Before every request, checks to see if any loaded modules have changed on
  1923. disk and, if so, reloads them.
  1924. """
  1925. def __init__(self, func):
  1926. self.func = func
  1927. self.mtimes = {}
  1928. global _compiletemplate
  1929. b = _compiletemplate.bases
  1930. _compiletemplate = globals()['__compiletemplate']
  1931. _compiletemplate.bases = b
  1932. def check(self):
  1933. for mod in sys.modules.values():
  1934. try:
  1935. mtime = os.stat(mod.__file__).st_mtime
  1936. except (AttributeError, OSError, IOError):
  1937. continue
  1938. if mod.__file__.endswith('.pyc') and \
  1939. os.path.exists(mod.__file__[:-1]):
  1940. mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
  1941. if mod not in self.mtimes:
  1942. self.mtimes[mod] = mtime
  1943. elif self.mtimes[mod] < mtime:
  1944. try:
  1945. reload(mod)
  1946. except ImportError:
  1947. pass
  1948. return True
  1949. def __call__(self, e, o):
  1950. self.check()
  1951. return self.func(e, o)
  1952. reloader = Reloader
  1953. def profiler(app):
  1954. """Outputs basic profiling information at the bottom of each response."""
  1955. def profile_internal(e, o):
  1956. out, result = profile(app)(e, o)
  1957. return out + ['<pre>' + result + '</pre>'] #@@encode
  1958. return profile_internal
  1959. ## Context
  1960. class _outputter:
  1961. """Wraps `sys.stdout` so that print statements go into the response."""
  1962. def write(self, string_):
  1963. if hasattr(ctx, 'output'):
  1964. return output(string_)
  1965. else:
  1966. _oldstdout.write(string_)
  1967. def flush(self):
  1968. return _oldstdout.flush()
  1969. def close(self):
  1970. return _oldstdout.close()
  1971. _context = {currentThread():Storage()}
  1972. ctx = context = threadeddict(_context)
  1973. ctx.__doc__ = """
  1974. A `storage` object containing various information about the request:
  1975. `environ` (aka `env`)
  1976. : A dictionary containing the standard WSGI environment variables.
  1977. `host`
  1978. : The domain (`Host` header) requested by the user.
  1979. `home`
  1980. : The base path for the application.
  1981. `ip`
  1982. : The IP address of the requester.
  1983. `method`
  1984. : The HTTP method used.
  1985. `path`
  1986. : The path request.
  1987. `fullpath`
  1988. : The full path requested, including query arguments.
  1989. ### Response Data
  1990. `status` (default: "200 OK")
  1991. : The status code to be used in the response.
  1992. `headers`
  1993. : A list of 2-tuples to be used in the response.
  1994. `output`
  1995. : A string to be used as the response.
  1996. """
  1997. if not '_oldstdout' in globals():
  1998. _oldstdout = sys.stdout
  1999. sys.stdout = _outputter()
  2000. loadhooks = {}
  2001. def load():
  2002. """
  2003. Loads a new context for the thread.
  2004. You can ask for a function to be run at loadtime by
  2005. adding it to the dictionary `loadhooks`.
  2006. """
  2007. _context[currentThread()] = Storage()
  2008. ctx.status = '200 OK'
  2009. ctx.headers = []
  2010. if 'db_parameters' in globals():
  2011. connect(**db_parameters)
  2012. for x in loadhooks.values(): x()
  2013. def _load(env):
  2014. load()
  2015. ctx.output = ''
  2016. ctx.environ = ctx.env = env
  2017. ctx.host = env.get('HTTP_HOST')
  2018. ctx.home = 'http://' + env.get('HTTP_HOST', '[unknown]') + \
  2019. os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))
  2020. ctx.ip = env.get('REMOTE_ADDR')
  2021. ctx.method = env.get('REQUEST_METHOD')
  2022. ctx.path = env.get('PATH_INFO')
  2023. # http://trac.lighttpd.net/trac/ticket/406 requires:
  2024. if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'):
  2025. ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0],
  2026. os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', '')))
  2027. ctx.fullpath = ctx.path
  2028. if env.get('QUERY_STRING'):
  2029. ctx.fullpath += '?' + env.get('QUERY_STRING', '')
  2030. unloadhooks = {}
  2031. def unload():
  2032. """
  2033. Unloads the context for the thread.
  2034. You can ask for a function to be run at loadtime by
  2035. adding it ot the dictionary `unloadhooks`.
  2036. """
  2037. for x in unloadhooks.values(): x()
  2038. # ensures db cursors and such are GCed promptly
  2039. del _context[currentThread()]
  2040. def _unload():
  2041. unload()
  2042. if __name__ == "__main__":
  2043. import doctest
  2044. doctest.testmod()
  2045. urls = ('/web.py', 'source')
  2046. class source:
  2047. def GET(self):
  2048. header('Content-Type', 'text/python')
  2049. print open(sys.argv[0]).read()
  2050. run(urls)
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注