"""This is the "standard library" for Aquarium's structure."""
__docformat__ = "restructuredtext"
# Created: Sat May 6 09:53:53 PDT 2000
# Author: Shannon -jj Behrens
# Email: jjinux@users.sourceforge.net
#
# Copyright (c) Shannon -jj Behrens. All rights reserved.
from cStringIO import StringIO
import math
import os
import re
from stat import ST_MTIME
import sys
import time
import traceback
import types
from AquariumClass import AquariumClass
import aquarium.conf.AquariumProperties as properties
import HTTPResponses
class InternalLibrary(AquariumClass):
"""This is the "standard library" for Aquarium's structure."""
def htmlEntities(self, s):
"""Return ``s`` after "HTML encoding" it (i.e. ``& -> &``, etc.).
I'm leaving this here just in case I have to do charset specific
entity encodings.
"""
translations = {"&": "&", '"': """, "<": "<", ">": ">"}
l = list(str(s))
for i in range(len(l)):
c = l[i]
l[i] = translations.get(c, c)
return "".join(l)
def javaScriptEscape(self, s, htmlent=True):
"""Prepare ``s`` for use in a JavaScript quoted string.
Both ``"`` and ``'`` are escaped, so you can use the return value in
either single or double quotes.
Test cases::
[('\x08', '\\b') # backspace
('\x09', '\\t') # horizontal tab
('\x0A', '\\n') # line feed (new line)
('\x0B', '\\v') # vertical tab
('\x0C', '\\f') # form feed
('\x0D', '\\r') # carriage return
('"', '\\"') # double quote
("'", "\\'") # single quote
('\\', '\\\\')] # backslash
"""
if not isinstance(s, unicode):
s = str(s) # Never call str on unicode.
esc_str = (
s.replace('\\', '\\\\')
.replace('\x08', '\\b')
.replace('\x09', '\\t')
.replace('\x0A', '\\n')
.replace('\x0B', '\\v')
.replace('\x0C', '\\f')
.replace('\x0D', '\\r')
.replace('"', '\\"')
.replace("'", "\\'")
)
if htmlent:
return self._ctx.htmlent(esc_str)
return esc_str
def javaScriptQuote(self, s, htmlent=True):
"""Escape ``s`` and wrap it in single quotes."""
return "'%s'" % self.javaScriptEscape(s, htmlent)
def validModuleName(self, name):
"""This is an alias for validModuleName_.
.. _validModuleName:
aquarium.util.InternalLibrary-module.html#validModuleName
"""
return validModuleName(name)
def aquariumFactory(self, moduleName, *args, **kargs):
"""Dynamically import and instantiate a class from Aquarium.
This works for templates too. In fact, this method will automatically
compile templates as necessary. Because it's difficult to distinguish
a compiled template from a normal Python module, you should not create
both a template and a normal Python module with the same name
(excluding the extension) in the same directory as many bad things can
happen.
I will also recursively compile base templates if you're real nice to
me and use lines that look like::
#extends aquarium.foo.Bar
Otherwise, this is a *very difficult* thing to do. In fact, I'll go so
far as to raise an exception if you try to extend something without
using a module name that starts with "aquarium." (i.e. a module name
that isn't fully specified).
moduleName
This is the name of the module relative to the aquarium package (e.g.
``screen.MyScreen``).
``*args``, ``**kargs``
These will be passed to the class's constructor. ``self._ctx`` will
always be added to the front of ``*args`` unless you specify the
keyword parameter ``aquariumFactoryNoContext=1`` (which will be
removed).
"""
# Add self._ctx to args, if appropriate.
if (kargs.has_key("aquariumFactoryNoContext") and
kargs["aquariumFactoryNoContext"]):
del kargs["aquariumFactoryNoContext"]
else:
args = (self._ctx,) + args
# Examine the moduleName.
moduleName = "aquarium." + moduleName
if not self.validModuleName(moduleName):
raise ValueError("Invalid module name", moduleName)
# Take care of compiling templates, if appropriate. Make sure that
# Cheetah is even installed, otherwise Aquarium just hangs and won't
# even throw an exception.
if (not sys.modules.has_key(moduleName) and
properties.CHEETAH_COMPILE):
import Cheetah
self._findAndCompile(moduleName)
# Now we're ready to do use __import__.
name = moduleName.split(".")[-1]
module = __import__(moduleName, {}, {}, [name])
return getattr(module, name)(*args, **kargs)
def _findAndCompile(self, moduleName):
"""Find and perhaps compile a class from Aquarium.
moduleName
Unlike ``aquariumFactory``, I expect this to start with "aquarium".
"""
# Examine the moduleName, and create all the different versions of it
# that we need.
pieces = moduleName.split(".")
package = ".".join(pieces[:-1]) # E.g. aquarium.screen.a
moduleTypePieces = pieces[1:-1] # E.g. ["screen", "a"]
parentModule = pieces[-2] # E.g. a
name = pieces[-1] # E.g. A
(packagePath, moduleCompileDir) = self._getModuleCompileDir(
package, parentModule, moduleTypePieces)
# Find the templateFile. If you can't find it, assume it's just
# a normal Python module and just return.
for dir in packagePath:
templateFile = os.path.join(dir, name + ".tmpl")
if os.path.isfile(templateFile):
break
else:
return
# Where do we expect to find the compiledTemplate?
if moduleCompileDir:
compiledTemplate = os.path.join(moduleCompileDir, name + ".py")
else:
compiledTemplate = re.sub(r"\.tmpl$", ".py", templateFile)
# Compile the template and its parent.
if self._needsCompiling(templateFile, compiledTemplate):
self._compile(moduleName, templateFile, compiledTemplate, name)
self._compileParent(templateFile)
def _getModuleCompileDir(self, package, parentModule, moduleTypePieces):
"""Return ``(packagePath, moduleCompileDir)``.
Make use of ``properties.CHEETAH_COMPILE_DIRECTORY``. If it is None,
``moduleCompileDir`` will also be None.
"""
packagePath = __import__(package, {}, {}, parentModule).__path__
if properties.CHEETAH_COMPILE_DIRECTORY:
moduleCompileDir = os.path.join(
properties.CHEETAH_COMPILE_DIRECTORY, *moduleTypePieces)
else:
moduleCompileDir = None
return (packagePath, moduleCompileDir)
def _needsCompiling(self, templateFile, compiledTemplate):
"""Does the template need to be compiled or recompiled?"""
return (not os.path.isfile(compiledTemplate) or
isModified(os.stat(compiledTemplate)[ST_MTIME],
os.stat(templateFile)[ST_MTIME]))
def _compile(self, moduleName, templateFile, compiledTemplate, name):
"""Create any directories necessary and compile the template.
The module will also contain a new attribute,
``__AQUARIUM_cheetahSrc__``, documenting where the original template
was found.
"""
from Cheetah.Compiler import Compiler
dir = os.path.dirname(compiledTemplate)
if not os.path.isdir(dir):
os.makedirs(dir)
f = open(compiledTemplate, "w")
try:
compiler = Compiler(file=templateFile, moduleName=name,
mainClassName=name)
f.write(str(compiler))
f.write("\n__AQUARIUM_cheetahSrc__ = r'%s'\n" % templateFile)
finally:
f.close()
def _compileParent(self, templateFile):
"""Recursively update the parent class, as necessary."""
f = open(templateFile)
try:
for line in f.readlines():
matches = re.match(r"^#extends ((\w|\.)+)", line)
if matches:
parentModuleName = matches.group(1)
if not parentModuleName.startswith("aquarium."):
msg = """\
It looks like you're trying to use "#extends" without a module name that starts
with "aquarium." . Since I can't handle that case, I'm going to go ahead and
raise an exception. I apologize for the inconvenience. See
InternalLibrary.aquariumFactory for more information. By the way, you were
trying to extend: %s."""
raise ImportError(msg % parentModuleName)
self._findAndCompile(parentModuleName)
finally:
f.close()
def call(self, moduleName, *args, **kargs):
"""This is a convenience method for ``aquariumFactory().__call__()``.
moduleName
This is the name of the module relative to the aquarium package (e.g.
``screen.MyScreen``).
``*args``, ``**kargs``
These will be passed to ``__call__``.
If you need to pass additional arguments to ``aquariumFactory`` or if
you need to call some other method than ``__call__``, don't use this
method.
"""
return self.aquariumFactory(moduleName)(*args, **kargs)
def inverseExtend(self, boundMethod, *args, **kargs):
"""Iterate downward through a hierarchy calling a method at each step.
In a sense, this is like the object oriented concept of extending a
method, except the parent class wraps the child class instead of the
other way around. This method was inspired by Perl's Mason_.
Just as with extending a method, you can pass whatever arguments you
want to "super" (although here you're passing those arguments to the
subclass via ``callNext``) and you can return whatever you want (the
highest level method returns to the actual caller).
If an instance has multiple superclasses, only the first is considered.
boundMethod
This is the bound method of the object you're interested in.
``*args``, ``**kargs``
The arguments and keyword arguments to pass to the top-level method.
You can call this method via something like this::
inverseExtend(object.method, myArg, myOtherArg)
When calling the method at each step, I'll call it like this::
Class.method(object, callNext, *args, **kargs)
However, the lowest level class's method has no ``callNext`` parameter,
since it has no one else to call::
Class.method(object, *args, **kargs)
In the method::
callNext(*args, **kargs)
should be called when it is time to transfer control to the subclass.
This may even be in the middle of the method. Naturally, you don't
have to pass ``*args``, ``**kargs``, but a common idiom is for the
parent class to just receive ``*args`` and ``**kargs`` and pass them on
unmodified.
.. _Mason: http://www.masonhq.com
"""
return _DescendClassHierarchyImpl(boundMethod)(*args, **kargs)
def forward(self, screen, *args, **kargs):
"""Forward processing to a new screen. This method does not return.
Generate a ``Forward`` exception which
aquarium.util.Aquarium.screenLoop_ class is prepared to catch.
screen
This is the module name of the screen relative to the screen
directory.
``*args``, ``**kargs``
The arguments to pass to the screen's ``__call__`` method.
.. _aquarium.util.Aquarium.screenLoop:
aquarium.util.Aquarium.Aquarium-class.html#screenLoop
"""
raise Forward(screen, *args, **kargs)
def redirect(self, url, httpResponse=HTTPResponses.TEMPORARY_REDIRECT):
"""Do an HTTP redirect. This method does not return.
url
The URL to redirect the user to.
httpResponse
The HTTP response code. See aquarium.util.HTTPResponses_.
Use this method if you must redirect to a url not on this site. In
most cases, you'll want redirectSeeOther instead.
(This works by doing a ``forward`` to aquarium.screen.redirect_.)
.. _aquarium.util.HTTPResponses: aquarium.util.HTTPResponses-module.html
.. _aquarium.screen.redirect:
aquarium.screen.redirect.redirect-class.html
"""
self.forward("redirect", url, httpResponse)
def redirectSeeOther(self, screen, *args, **kargs):
"""Do an HTTP redirect using ``HTTPResponses.SEE_OTHER``.
This is a convenience method for::
import HTTPResponses
url = ctx.url.screen(screen, *args, **kargs)
ctx.iLib.redirect(url, HTTPResponses.SEE_OTHER)
This method does not return.
Sometimes you have a form (e.g. a login form), and after the user
submits the form, you redirect him to some other screen within the
site. That's what the HTTP response "303 See Other" is made for, and
hence, that's what this method is made for.
"""
ctx = self._ctx
url = ctx.url.screen(screen, *args, **kargs)
ctx.iLib.redirect(url, HTTPResponses.SEE_OTHER)
def redirectTemporary(self, screen, *args, **kargs):
"""Do an HTTP redirect using ``HTTPResponses.TEMPORARY_REDIRECT``.
This is a convenience method for::
import HTTPResponses
url = ctx.url.screen(screen, *args, **kargs)
ctx.iLib.redirect(url, HTTPResponses.TEMPORARY_REDIRECT)
This method does not return.
Use this method instead of redirectSeeOther if you want to use a
TEMPORARY_REDIRECT instead of a SEE_OTHER redirect. In most cases,
you'll want redirectSeeOther instead.
"""
ctx = self._ctx
url = ctx.url.screen(screen, *args, **kargs)
ctx.iLib.redirect(url, HTTPResponses.TEMPORARY_REDIRECT)
def validModuleName(name):
"""Return true if the given module name is valid.
This is valid: ``foo.bar``
These are not: ``.foo.bar``, ``foo..bar``, ``foo/bar``, etc.
Please note that externally, screens use "/"'s, but this function
expects those to have been changed to "."'s.
"""
return cacheByArgs(_validModuleNameUncached, name)
def _validModuleNameUncached(name):
pieces = name.split(".")
for i in pieces:
if not re.match(r"^\w+$", i):
return False
return True
_lastModuleUpdate = time.time()
def clearModules():
"""Clear ``sys.modules`` of specific types of modules if one is stale.
See ``properties.CLEAR_MODULES``.
I took this method out of the ``InternalLibrary`` class so that you can
call it *really* early, even before you create a ``Context`` to pass to
``InternalLibrary``.
This function also calls ``clearGetTextTranslations``.
History
-------
The problem that this method solves is simple: if I change a file, I don't
want to have to restart the server. It's a simple problem, but it's tough
to implement right. To prevent repeating mistakes, here's what has failed
in the past:
* Remove all modules from ``sys.modules`` on every page load.
- Some modules have state.
* Delete only those modules that don't have state.
- There's no convenient way to know which ones have state.
* Use module attributes.
- It's not convenient.
* Delete only those modules that have grown stale.
- If a parent class gets reloaded, child classes in other modules will
need to get reloaded, but we don't know which modules those classes are
in.
* Look through all the modules for module references to the modules
that'll get deleted and delete those too.
- Its very common to only import the class, not the whole module. Hence,
we still don't know which modules have child classes that need to get
reloaded.
* Just clear out ``sys.modules`` of all modules of certain types on every
page load.
- Even a moderate amount of kiddie clicking will result in exceptions.
I think the browsers hide the problem, but you'll see the exceptions
in the logs.
* Clear out ``sys.modules`` of all modules of certain types on every page
load, but only if at least one of the modules is stale.
- This is good because it handles the kiddie clicking case, and it also
handles the super class case.
* We need to handle stale templates too. ``aquariumFactory`` use to do
this, but it suffered from the same super class problem. Hence, if a
``.tmpl`` has been updated, its corresponding ``.py`` must be deleted.
The other ``.py`` files don't need to be deleted. However, all the
``sys.modules`` need to be cleared, just in case some class was using a
superclass from that ``.py``.
"""
def unlinkPyPyc(file):
"""Given a ``.py`` or ``.pyc``, delete both."""
toDelete = [file]
if file.endswith(".py"):
toDelete.append(file + "c")
else:
toDelete.append(file[:-1])
for i in toDelete:
if os.path.exists(i):
os.unlink(i)
global _lastModuleUpdate
clearGetTextTranslations()
if not properties.CLEAR_MODULES:
return
# Figure out which modules are subject to getting deleted.
deleteTheseTypes = properties.CLEAR_MODULES
if not isinstance(deleteTheseTypes, list):
# Update Seamstress Exchange's properties file if you change this.
deleteTheseTypes = ["aquarium.layout","aquarium.navigation",
"aquarium.screen", "aquarium.widget"]
assert not "aquarium.util" in deleteTheseTypes, """
Sorry, it's not possible to reload aquarium.util modules. Afterall,
aquarium.util.Aquarium and aquarium.util.InternalLibrary are the core of
Aquarium. Please update AquariumProperties.CLEAR_MODULES."""
deleteThese = [
moduleName
for moduleType in deleteTheseTypes
for moduleName in sys.modules.keys()
if (moduleName == moduleType or
moduleName.startswith(moduleType + "."))
]
# Are there any stale modules?
staleModules = False
for i in deleteThese:
module = sys.modules[i]
try:
file = module.__file__
except AttributeError:
continue
if file.endswith(".pyc") and os.path.exists(file[:-1]):
file = file[:-1]
if not os.path.exists(file):
staleModules = True
continue
fileMTime = os.stat(file)[ST_MTIME]
if isModified(_lastModuleUpdate, fileMTime):
staleModules = True
# Delete any stale compiled templates.
if (properties.CHEETAH_COMPILE and
hasattr(module, "__AQUARIUM_cheetahSrc__")):
template = module.__AQUARIUM_cheetahSrc__
if os.path.exists(template):
templateMTime = os.stat(template)[ST_MTIME]
if isModified(fileMTime, templateMTime):
unlinkPyPyc(file)
staleModules = True
# Delete the stale modules. I do this in a separate loop because you
# can't modify a dictionary during an iteration.
if staleModules:
for i in deleteThese:
del sys.modules[i]
_lastModuleUpdate = time.time()
_lastGetTextUpdate = time.time()
def clearGetTextTranslations():
"""Clear ``gettext`` of stale translations.
Because translation instances can create a chain of interrelated fallbacks,
if even one of them is stale, I clear them all.
This setting is controlled by ``properties.CLEAR_GETTEXT``. However, if
that is undefined (which is likely since it's only mentioned here), I will
use the value::
properties.CLEAR_MODULES and properties.USE_GETTEXT
"""
global _lastGetTextUpdate
default = properties.CLEAR_MODULES and properties.USE_GETTEXT
if not getattr(properties, "CLEAR_GETTEXT", default):
return
import gettext
for file in gettext._translations.keys():
if isModified(_lastGetTextUpdate, os.stat(file)[ST_MTIME]):
gettext._translations.clear()
break
_lastGetTextUpdate = time.time()
def isModified(lastUpdate, currentValue):
"""Is ``math.floor(lastUpdate) < math.floor(currentValue)``?
lastUpdate
This is either a timestamp or a ``stat[ST_MTIME]``.
currentValue
This is a ``stat[ST_MTIME]``.
``math.floor`` is used because the resolution of ``time.time()`` is higher
than that of ``stat[ST_MTIME]`` on some operating systems / filesystems.
"""
return math.floor(lastUpdate) < math.floor(currentValue)
def passStringIO(toArg, f, *args, **kargs):
"""Call ``f`` with a ``StringIO`` instead of an output file.
toArg
This is the name of one of ``f``'s parameters. Create a new StringIO
object and pass it in for this parameter.
f
This is some function that takes an output file.
args, kargs
These will also be passed to ``f``.
When ``f`` completes, serialize the StringIO to a string, and return that.
This function is really useful when working with
``traceback.print_exception``.
"""
buf = StringIO()
try:
kargs[toArg] = buf
f(*args, **kargs)
value = buf.getvalue()
return value
finally:
buf.close()
def getExceptionStr(etype=None, value=None, tb=None, *args, **kargs):
"""This is a convenience method for ``traceback.print_exception``.
etype, value, tb
If you specify these, I'll call ``traceback.print_exception``.
Otherwise, I'll call ``traceback.print_exc``.
args
These are additional arguments to ``print_exc`` or ``print_exception``.
Instead of accepting an output file, I'll use a ``StringIO`` and then
return the results as a string. That's the one bit of convenience I add.
"""
if etype is None:
f = traceback.print_exc
else:
f = traceback.print_exception
args = (etype, value, tb) + args
return passStringIO("file", f, *args, **kargs)
def cacheByArgs(f, *args):
"""Cache the results of ``f``. Vary by ``args``.
The cache will be stored as an attribute of ``f`` named ``_cacheByArgs``.
Note that everything in ``args`` must be hashable (this implies
non-modifiable) because ``args`` is used as a dictionary key. For this
same reason, I can't support ``**kargs`` (which would probably be pretty
slow anyway).
Since the cache is attached to the function, it lives as long as the
function does. If you need to throw the cache away on every request, use a
new lambda for every request.
Use this function very carefully. Make sure you're not over-zealous in
your cacheing; some things should not be cached. Also, make sure your
cache doesn't just grow infinitely; you should test to make sure you don't
create a memory leak. When you do test, remember to turn sessions off, or
else you'll mistake the creation of a lot of sessions for a memory leak.
"""
if not hasattr(f, "_cacheByArgs"):
f.__dict__["_cacheByArgs"] = {}
cache = f._cacheByArgs
if not cache.has_key(args):
cache[args] = f(*args)
return cache[args]
class _DescendClassHierarchyImpl:
"""Implement the inverseExtend method.
There are many ways to implement this method. One involves the use of
yield. It doesn't require a ``callNext`` parameter, but it is not possible
to use such an implementation in a function that has a return statement
(which Cheetah methods always do, unfortunately). Another involves the use
of a closure. This is elegant, but not viable if you are stuck with an old
version of Python. The implementation here is based on a class that has a
__call__ method and can thus pretend to be a closure. I may switch back to
the closure version of this code once they upgrade the version of Python at
work.
Do not use this class directly, rather use
aquarium.util.InternalLibrary.inverseExtend_.
The following private variables are used:
_obj
This is the instance that the bound method belongs to.
_methods
This will contain the methods from the *least* senior classes first
because it will be treated as a stack in ``__call__``.
.. _aquarium.util.InternalLibrary.inverseExtend:
aquarium.util.InternalLibrary.InternalLibrary-class.html#inverseExtend
"""
def __init__(self, boundMethod):
"""Build all the necessary data structures."""
self._obj = boundMethod.im_self
methodName = boundMethod.im_func.__name__
# Figure out the classes in the class hierarchy. "classes" will
# contain the most senior classes first.
Class = self._obj.__class__
classes = [Class]
while Class.__bases__:
Class = Class.__bases__[0]
classes.insert(0, Class)
# Skip classes that don't define the method. Be careful with getattr
# since it automatically looks in parent classes. Be careful of
# Python's new-style classes which return method-wrapper objects when
# it's not possible to return an actual method.
last = None
self._methods = []
for Class in classes:
if hasattr(Class, methodName):
method = getattr(Class, methodName)
if type(method) == types.MethodType and method != last:
last = method
self._methods.insert(0, method)
def __call__(self, *args, **kargs):
"""This is like ``super()``, but it calls the subclass's method.
See aquarium.util.InternalLibrary.inverseExtend_. Warning, this is a
bound method that acts like a closure.
.. _aquarium.util.InternalLibrary.inverseExtend:
aquarium.util.InternalLibrary.InternalLibrary-class.html#inverseExtend
"""
method = self._methods.pop()
if len(self._methods):
return method(self._obj, self.__call__, *args, **kargs)
else:
return method(self._obj, *args, **kargs)
class Forward(Exception):
"""Raise an instance of this class to make Aquarium do a forward.
Actually, you should use aquarium.util.InternalLibrary.forward_ to do that
for you.
The following attributes are used:
screen
The module name of the screen relative to the ``screen`` directory
``*args``, ``**kargs``
The arguments to pass to the screen's ``__call__`` method.
.. _aquarium.util.InternalLibrary.forward:
aquarium.util.InternalLibrary.InternalLibrary-class.html#forward
"""
def __init__(self, screen, *args, **kargs):
"""Just accept the parameters."""
Exception.__init__(self)
self.screen = screen
self.args = args
self.kargs = kargs
class FormValueError(ValueError):
"""Subclass ``ValueError`` for form errors that shouldn't occur.
I.e. this is for errors that are only the result of:
a) the user hacking stuff.
b) the programmer messing up.
"""
pass
|