summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--htdocs/images/carbon.pngbin0 -> 523 bytes
-rw-r--r--htdocs/images/jabber_logo.pngbin0 -> 4336 bytes
-rw-r--r--htdocs/images/left-sm.pngbin0 -> 10403 bytes
-rw-r--r--htdocs/images/right-sm.pngbin0 -> 10413 bytes
-rw-r--r--htdocs/main.py34
-rw-r--r--py-bin/config.py44
-rw-r--r--py-bin/ejabberdctl.py47
-rw-r--r--py-bin/jabberman.py392
-rw-r--r--py-bin/lib/__init__.py0
-rwxr-xr-xpy-bin/lib/em.py3288
-rw-r--r--py-bin/lib/jon/__init__.py3
-rw-r--r--py-bin/lib/jon/cgi.py673
-rw-r--r--py-bin/lib/jon/session.py209
-rw-r--r--py-bin/login.py24
-rw-r--r--py-bin/mail_auth.py111
-rw-r--r--py-bin/main.py31
-rw-r--r--py-bin/setup.py140
-rw-r--r--py-bin/templates/add_account_form.em17
-rw-r--r--py-bin/templates/delete_account_ask.em8
-rw-r--r--py-bin/templates/error.em13
-rw-r--r--py-bin/templates/jman_base.em94
-rw-r--r--py-bin/templates/jman_setup_base.em18
-rw-r--r--py-bin/templates/logged_out.em5
-rw-r--r--py-bin/templates/login_fail.em22
-rw-r--r--py-bin/templates/login_form.em26
-rw-r--r--py-bin/templates/mail_error.em16
-rw-r--r--py-bin/templates/mail_form.em24
-rw-r--r--py-bin/templates/mail_message.em21
-rw-r--r--py-bin/templates/mail_success.em15
-rw-r--r--py-bin/templates/mail_trylater.em16
-rw-r--r--py-bin/templates/set_pw_form.em29
-rw-r--r--py-bin/templates/setup_help.em75
-rw-r--r--py-bin/templates/setup_main.em22
-rw-r--r--py-bin/utils.py71
34 files changed, 5488 insertions, 0 deletions
diff --git a/htdocs/images/carbon.png b/htdocs/images/carbon.png
new file mode 100644
index 0000000..a72416c
--- /dev/null
+++ b/htdocs/images/carbon.png
Binary files differ
diff --git a/htdocs/images/jabber_logo.png b/htdocs/images/jabber_logo.png
new file mode 100644
index 0000000..f1b2d77
--- /dev/null
+++ b/htdocs/images/jabber_logo.png
Binary files differ
diff --git a/htdocs/images/left-sm.png b/htdocs/images/left-sm.png
new file mode 100644
index 0000000..5754a6d
--- /dev/null
+++ b/htdocs/images/left-sm.png
Binary files differ
diff --git a/htdocs/images/right-sm.png b/htdocs/images/right-sm.png
new file mode 100644
index 0000000..fda4da2
--- /dev/null
+++ b/htdocs/images/right-sm.png
Binary files differ
diff --git a/htdocs/main.py b/htdocs/main.py
new file mode 100644
index 0000000..5921752
--- /dev/null
+++ b/htdocs/main.py
@@ -0,0 +1,34 @@
+#main url mapper
+
+import sys
+sys.path.append("../py-bin")
+
+from utils import BasicHandler, process_request, set_logging_defaults
+from jabberman import JabberManager
+from login import LoginMixIn
+from mail_auth import MailAuthMixIn
+from setup import SetupMixIn
+
+set_logging_defaults()
+
+class MainHandler(BasicHandler, MailAuthMixIn, LoginMixIn, SetupMixIn):
+ def do_process(self, req):
+ command = req.params.get("cmd", "")
+
+ if command == "":
+ self.login_form(req)
+ else:
+ if hasattr(self, command):
+ method = getattr(self, command)
+ if hasattr(method, 'web_callable') and method.web_callable:
+ self.jman = JabberManager(self.session)
+ method(req)
+ else:
+ self.invalid_page(req)
+ else:
+ self.invalid_page(req)
+
+ def invalid_page(self, req):
+ self.error_page(req, "Ungueltiger Request.")
+
+process_request(MainHandler)
diff --git a/py-bin/config.py b/py-bin/config.py
new file mode 100644
index 0000000..2dd0fe8
--- /dev/null
+++ b/py-bin/config.py
@@ -0,0 +1,44 @@
+#webreg global config file
+
+# debug mode: for testing only!!!
+# never ever activate on production server!
+debugmode = True #x
+
+# mail configuration:
+mail_from_addr = "jabber@immerda.ch"
+# to use sendmail for delivery, use this:
+use_sendmail = True
+# to use python smtplib, configure these smtp options
+#smtp_server = "sicher.immerda.ch"
+# if you comment out user/pass, no smtp auth will be done
+#smtp_user = ""
+#smtp_pass = ""
+
+# domain name policy
+mail_domains = ["immerda.ch", "cronopios.org", "einfachsicher.ch"]
+extra_domains = ["imsg.ch", "unerkenntli.ch", "auchno.ch"] #x adapt to existing ones!
+# username and password policy
+user_re ='^[a-zA-Z0-9_\-.]+$'
+password_re = '^[a-z0-9_\-.]+$'
+min_password_length = debugmode and 2 or 8
+
+# path configuration
+# keep templates and all code except main.py outside doc-root!
+template_dir = "/e/e1/var/www/jabber/py-bin/templates"
+# keep the following stuff on an encrypted fs! (especially sessions)
+session_dir = "/e/e1/tmp"
+jabberdb_path = "/e/e1/var/db/jabberman/jabber_db"
+logfile_path = "/e/e1/var/log/lighttpd/webreg.log"
+
+# self reference, needed for http redirects
+#script_url = "https://jabber.immerda.ch:60843/main.py"
+script_url = "https://127.0.0.1:8000/main.py"
+
+# secret needed for session ids and registration tokens
+the_secret = "w1bXM13wHSI5BWrtMs97Cwxf7qWjL1xu"
+
+#experimental stuff...
+#x check again
+ejabberdctl_path = "/e/e1/opt/ejabberd/bin/ejabberdctl"
+ejabberdctl_environ = {"PATH":"/bin:/usr/bin", "HOME":"/e/e1/home/lighttpd"}
+
diff --git a/py-bin/ejabberdctl.py b/py-bin/ejabberdctl.py
new file mode 100644
index 0000000..a1ef5ff
--- /dev/null
+++ b/py-bin/ejabberdctl.py
@@ -0,0 +1,47 @@
+#ejabberdctl
+
+import subprocess, logging
+import config
+
+#x remove
+#def __run(path, params, env):
+# params = [path] + params
+# try:
+# result = os.spawnve(os.P_WAIT, path, params, env)
+# return (result == 0)
+# except Exception:
+# return False
+
+class EJabberdCtl:
+ def create_account(self, user, server, password):
+ if self.__ejabberdctl(["register", user, server, password]):
+ logging.info("Created account %s@%s." % (user, server))
+ return True
+ return False
+
+ def remove_account(self, user, server):
+ if self.__ejabberdctl(["unregister", user, server]):
+ logging.info("Removed account %s@%s." % (user, server))
+ return True
+ return False
+
+ def change_password(self, user, server, password):
+ if self.__ejabberdctl(["set-password", user, server, password]):
+ logging.info("Changed Password for %s@%s." % (user, server))
+ return True
+ return False
+
+ def __ejabberdctl(self, params):
+ return self.__run([config.ejabberdctl_path] + params, config.ejabberdctl_environ)
+
+ def __run(self, path_and_params, environ={}):
+ try:
+ result = subprocess.call(path_and_params, env=environ)
+ if result != 0:
+ logging.error("Error invoking '%s': Result = %s." %
+ (str(path_and_params), str(result)))
+ return (result == 0)
+ except Exception, e:
+ logging.error("Error invoking '%s': %s." % (str(path_and_params), str(e)))
+ return False
+
diff --git a/py-bin/jabberman.py b/py-bin/jabberman.py
new file mode 100644
index 0000000..11e3bdf
--- /dev/null
+++ b/py-bin/jabberman.py
@@ -0,0 +1,392 @@
+#jabber manager
+
+import shelve, atexit, sha, hmac, random, os, time, re
+import config
+from ejabberdctl import EJabberdCtl
+
+class JabberUser:
+ def __init__(self, user_id):
+ self.user, self.domain = user_id.split("@")
+ self.accounts = []
+
+ def get_user_id(self):
+ return self.user + "@" + self.domain
+
+ def get_default_jabber_id(self):
+ return self.user + "@jabber." + self.domain
+
+ def is_active(self):
+ return hasattr(self, "password_hash")
+
+ def check_password(self, password):
+ if not self.is_active():
+ return False
+ return self.password_hash == self.__hash_password(password)
+
+ def set_password(self, password):
+ self.password_hash = self.__hash_password(password)
+
+ def __hash_password(self, password):
+ return sha.new(password).hexdigest()
+
+ @staticmethod
+ def generate_token():
+ data = str(random.getrandbits(256)) + str(time.time()*1000) + str(os.getpid())
+ return "+" + hmac.new(config.the_secret, data, sha).hexdigest()
+
+ def set_token(self, token):
+ self.token = token
+
+ def validate_token(self, token):
+ if token[1:] == self.token[1:]:
+ if self.__is_token_expired():
+ return (False, "Benutzerkonto bereits aktiviert.")
+ return (True, self)
+ else:
+ return (False, "Zugriff verweigert.")
+
+ def __is_token_expired(self):
+ return self.token[0] != "+"
+
+ def expire_token(self):
+ self.token = "-" + self.token[1:]
+
+ def add_account(self, jabber_id):
+ self.accounts.append(jabber_id)
+
+ def has_account(self, jabber_id):
+ return jabber_id in self.accounts
+
+ def get_account_list(self):
+ return list(self.accounts)
+
+ def get_extra_account_list(self):
+ default_acc = self.get_default_jabber_id()
+ return filter(lambda acc: acc != default_acc, self.accounts)
+
+ def remove_account(self, jabber_id):
+ self.accounts.remove(jabber_id)
+
+class JabberAccount:
+ def __init__(self, jabber_id):
+ self.user, self.server = jabber_id.split("@")
+
+ def get_jabber_id(self):
+ return self.user + "@" + self.server
+
+class JabberDB:
+ def __init__(self):
+ self.db = shelve.open(config.jabberdb_path, 'c')
+ atexit.register(self.db.close)
+
+ def login_user(self, user_id, password):
+ user = self.__load_user(user_id)
+ if user and user.check_password(password):
+ return user
+ return None
+
+ def generate_token(self, user_id):
+ if self.__load_user(user_id):
+ return (False, "Benutzer existiert bereits!")
+
+ return (True, JabberUser.generate_token())
+
+ def prepare_user(self, user_id, token):
+ if self.__load_user(user_id):
+ return (False, "Benutzer existiert bereits!")
+
+ user = JabberUser(user_id)
+ user.set_token(token)
+ self.__store_user(user)
+
+ return (True, "Benutzer registriert, Aktivierung noch ausstehend.")
+
+ def validate_token(self, user_id, token):
+ user = self.__load_user(user_id)
+ if not user:
+ return (False, "Zugriff verweigert.")
+
+ return user.validate_token(token)
+
+ def activate_user(self, user_id, password, token):
+ user = self.__load_user(user_id)
+ if not user:
+ return (False, "Zugriff verweigert.")
+
+ ok, status = user.validate_token(token)
+ if not ok:
+ return (False, status)
+
+ user.expire_token()
+ user.set_password(password)
+ self.__store_user(user)
+
+ return (True, user)
+
+ def add_account(self, user_id, jabber_id, check_only = False):
+ user = self.__load_user(user_id)
+ if not user:
+ return (False, "Zugriff verweigert.")
+
+ account = self.__load_account(jabber_id)
+ if account:
+ return (False, "Sorry, Jabber Benutzerkonto %s bereits vergeben." % jabber_id)
+
+ if check_only:
+ return (True, "Jabber kann hinzugefuegt werden.")
+
+ account = JabberAccount(jabber_id)
+ self.__store_account(account)
+ user.add_account(jabber_id)
+ self.__store_user(user)
+
+ return (True, "Jabber Konto hinzugefuegt.")
+
+ def remove_account(self, user_id, jabber_id, check_only = False):
+ user = self.__load_user(user_id)
+ if (not user) or (not user.has_account(jabber_id)):
+ return (False, "Zugriff verweigert.")
+
+ if check_only:
+ return (True, "Jabber darf geloescht werden.")
+
+ self.__delete_account(jabber_id)
+ user.remove_account(jabber_id)
+ self.__store_user(user)
+
+ return (True, "Jabber Konto geloescht.")
+
+ def change_password(self, user_id, password):
+ user = self.__load_user(user_id)
+ if not user:
+ return (False, "Zugriff verweigert.")
+
+ user.set_password(password)
+ self.__store_user(user)
+ return (True, "Passwort geaendert.")
+
+ def __load_user(self, user_id):
+ return self.db.get("#usr#" + user_id)
+
+ def __store_user(self, user):
+ self.db["#usr#" + user.get_user_id()] = user
+
+ def __load_account(self, jabber_id):
+ return self.db.get("#acc#" + jabber_id)
+
+ def __store_account(self, account):
+ self.db["#acc#" + account.get_jabber_id()] = account
+
+ def __delete_account(self, jabber_id):
+ del(self.db["#acc#" + jabber_id])
+
+
+class JabberManager:
+ def __init__(self, session):
+ self.jadb = JabberDB()
+ self.session = session
+ self.current_user, self.authenticated = None, False
+ self.ejctl = EJabberdCtl()
+
+ def get_user(self):
+ return self.current_user
+
+ def authenticate(self):
+ if self.authenticated == True:
+ return True
+ if (not "uid" in self.session) or (not "pass" in self.session):
+ return (False, "Nicht angemeldet.")
+ ok, status_or_user = self.login(
+ self.session["uid"], self.session["pass"])
+ return (ok, status_or_user)
+
+ def login(self, user_id, password):
+ ok, status = self.check_user_id(user_id)
+ if not ok:
+ return (False, status)
+
+ self.current_user = self.jadb.login_user(user_id, password)
+ if self.current_user:
+ self.__set_session(user_id, password = password)
+ else:
+ self.__clear_session()
+ return (False, "Benutzername oder Passwort falsch.")
+
+ self.authenticated = True
+ return (True, self.current_user)
+
+ def logout(self):
+ self.current_user, self.authenticated = None, False
+ self.__clear_session()
+
+ def generate_token(self, user_id):
+ ok, status = self.check_user_id(user_id)
+ if not ok:
+ return (False, status)
+
+ return self.jadb.generate_token(user_id)
+
+ def prepare_user(self, user_id, token):
+ ok, status = self.check_user_id(user_id)
+ if not ok:
+ return (False, status)
+
+ return self.jadb.prepare_user(user_id, token)
+
+ def validate_token(self, user_id, token):
+ if user_id == "":
+ try:
+ user_id, token = self.session["uid"], self.session["tok"]
+ except Exception:
+ return (False, "Zugriff verweigert.")
+
+ ok, status = self.check_user_id(user_id)
+ if not ok:
+ return (False, "Zugriff verweigert.")
+
+ ok, status_or_user = self.jadb.validate_token(user_id, token)
+ if ok:
+ self.current_user = status_or_user
+ self.__set_session(user_id, token = token)
+ return (ok, status_or_user)
+
+ def activate_user(self, password):
+ try:
+ user_id, token = self.session["uid"], self.session["tok"]
+ except Exception:
+ return (False, "Zugriff verweigert.")
+ ok, status = self.check_user_id(user_id)
+ if not ok:
+ return (False, "Zugriff verweigert.")
+
+ ok, status_or_user = self.jadb.activate_user(user_id, password, token)
+ if ok:
+ self.current_user, self.authenticated = status_or_user, True
+ self.__set_session(user_id, password = password)
+ else:
+ self.__clear_session()
+ return (False, status_or_user)
+
+ ok, status = self.add_account(self.current_user.get_default_jabber_id())
+ if not ok:
+ #todo: handle this smarter somehow
+ return (False, status)
+
+ return (True, status)
+
+ def change_password(self, password):
+ if not self.authenticated:
+ return (False, "Zugriff verweigert.")
+
+ user_id = self.current_user.get_user_id()
+ ok, status = self.jadb.change_password(user_id, password)
+ if ok:
+ self.__set_session(user_id, password = password)
+ else:
+ self.__clear_session()
+ return (False, status)
+
+ for jabber_id in self.current_user.get_account_list():
+ acc = JabberAccount(jabber_id)
+ if not self.ejctl.change_password(acc.user, acc.server, password):
+ msg = "Konnte Jaber Passwort fuer %s nicht setzen." % acc.get_jabber_id()
+ return (False, msg)
+
+ return (True, "Passwort erfolgreich geaendert.")
+
+ def is_acceptable_password(self, password, password2):
+ if password != password2:
+ return (False, "Passwoerter nicht identisch.")
+ if len(password) < config.min_password_length:
+ return (False, "Passwort ist zu kurz.")
+ if not re.match(config.password_re, password):
+ return (False, "Passwort enthaelt unerlaubte Zeichen.")
+ return (True, "Passwort OK.")
+
+ def add_account(self, jabber_id):
+ if not self.authenticated:
+ return (False, "Zugriff verweigert.")
+
+ ok, status = JabberManager.check_jabber_id(jabber_id)
+ if not ok:
+ return (False, status)
+
+ acc = JabberAccount(jabber_id)
+ try:
+ password = self.session["pass"]
+ except Exception:
+ return (False, "Zugriff verweigert.")
+
+ user_id = self.current_user.get_user_id()
+ ok, status = self.jadb.add_account(user_id, jabber_id, check_only = True)
+ if not ok:
+ return (False, status)
+
+ if not self.ejctl.create_account(acc.user, acc.server, password):
+ return (False, "Konnte Konto %s nicht erstellen." % acc.get_jabber_id())
+
+ user_id = self.current_user.get_user_id()
+ return self.jadb.add_account(user_id, jabber_id)
+
+ def remove_account(self, jabber_id):
+ if not self.authenticated:
+ return (False, "Zugriff verweigert.")
+
+ ok, status = JabberManager.check_jabber_id(jabber_id)
+ if not ok:
+ return (False, "Zugriff verweigert.")
+
+ user_id = self.current_user.get_user_id()
+ if jabber_id == self.current_user.get_default_jabber_id():
+ return (False, "Hauptkonto darf nicht geloescht werden!")
+
+ ok, status = self.jadb.remove_account(user_id, jabber_id, check_only = True)
+ if not ok:
+ return (False, status)
+
+ acc = JabberAccount(jabber_id)
+ if not self.ejctl.remove_account(acc.user, acc.server):
+ return (False, "Konnte Konto %s nicht loeschen." % acc.get_jabber_id())
+
+ return self.jadb.remove_account(user_id, jabber_id)
+
+ def __set_session(self, user_id, password = None, token = None):
+ self.__clear_session()
+ self.session["uid"] = user_id
+ if password:
+ self.session["pass"] = password
+ if token:
+ self.session["tok"] = token
+
+ def __clear_session(self):
+ if self.session.get("uid"):
+ del(self.session["uid"])
+ if self.session.get("pass"):
+ del(self.session["pass"])
+ if self.session.get("token"):
+ del(self.session["tok"])
+
+ @staticmethod
+ def __check_account_form(acc_type, domains, account):
+ try:
+ user, domain = account.split("@")
+ except ValueError:
+ status = "Ungueltige %s Adresse. Erwartete Form: user@domain." % acc_type
+ return (False, status)
+
+ if not re.match(config.user_re, user):
+ return (False, "Benutzername %s nicht erlaubt." % user)
+ if domain not in domains:
+ return (False, "Domain %s nicht erlaubt." % domain)
+ return (True, "%s Adresse akzeptiert." % acc_type)
+
+ @staticmethod
+ def check_user_id(user_id):
+ return JabberManager.__check_account_form("E-Mail", config.mail_domains, user_id)
+
+ @staticmethod
+ def check_jabber_id(jabber_id):
+ domains = map(lambda d: "jabber." + d, config.mail_domains) + config.extra_domains
+ return JabberManager.__check_account_form("Jabber", domains, jabber_id)
+
+
diff --git a/py-bin/lib/__init__.py b/py-bin/lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/py-bin/lib/__init__.py
diff --git a/py-bin/lib/em.py b/py-bin/lib/em.py
new file mode 100755
index 0000000..6079316
--- /dev/null
+++ b/py-bin/lib/em.py
@@ -0,0 +1,3288 @@
+#!/usr/local/bin/python
+#
+# $Id: //projects/empy/em.py#146 $ $Date: 2003/10/27 $
+
+"""
+A system for processing Python as markup embedded in text.
+"""
+
+
+__program__ = 'empy'
+__version__ = '3.3'
+__url__ = 'http://www.alcyone.com/software/empy/'
+__author__ = 'Erik Max Francis <max@alcyone.com>'
+__copyright__ = 'Copyright (C) 2002-2003 Erik Max Francis'
+__license__ = 'LGPL'
+
+
+import copy
+import getopt
+import os
+import re
+import string
+import sys
+import types
+
+try:
+ # The equivalent of import cStringIO as StringIO.
+ import cStringIO
+ StringIO = cStringIO
+ del cStringIO
+except ImportError:
+ import StringIO
+
+# For backward compatibility, we can't assume these are defined.
+False, True = 0, 1
+
+# Some basic defaults.
+FAILURE_CODE = 1
+DEFAULT_PREFIX = '@'
+DEFAULT_PSEUDOMODULE_NAME = 'empy'
+DEFAULT_SCRIPT_NAME = '?'
+SIGNIFICATOR_RE_SUFFIX = r"%(\S+)\s*(.*)\s*$"
+SIGNIFICATOR_RE_STRING = DEFAULT_PREFIX + SIGNIFICATOR_RE_SUFFIX
+BANGPATH = '#!'
+DEFAULT_CHUNK_SIZE = 8192
+DEFAULT_ERRORS = 'strict'
+
+# Character information.
+IDENTIFIER_FIRST_CHARS = '_abcdefghijklmnopqrstuvwxyz' \
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+IDENTIFIER_CHARS = IDENTIFIER_FIRST_CHARS + '0123456789.'
+ENDING_CHARS = {'(': ')', '[': ']', '{': '}'}
+
+# Environment variable names.
+OPTIONS_ENV = 'EMPY_OPTIONS'
+PREFIX_ENV = 'EMPY_PREFIX'
+PSEUDO_ENV = 'EMPY_PSEUDO'
+FLATTEN_ENV = 'EMPY_FLATTEN'
+RAW_ENV = 'EMPY_RAW_ERRORS'
+INTERACTIVE_ENV = 'EMPY_INTERACTIVE'
+BUFFERED_ENV = 'EMPY_BUFFERED_OUTPUT'
+NO_OVERRIDE_ENV = 'EMPY_NO_OVERRIDE'
+UNICODE_ENV = 'EMPY_UNICODE'
+INPUT_ENCODING_ENV = 'EMPY_UNICODE_INPUT_ENCODING'
+OUTPUT_ENCODING_ENV = 'EMPY_UNICODE_OUTPUT_ENCODING'
+INPUT_ERRORS_ENV = 'EMPY_UNICODE_INPUT_ERRORS'
+OUTPUT_ERRORS_ENV = 'EMPY_UNICODE_OUTPUT_ERRORS'
+
+# Interpreter options.
+BANGPATH_OPT = 'processBangpaths' # process bangpaths as comments?
+BUFFERED_OPT = 'bufferedOutput' # fully buffered output?
+RAW_OPT = 'rawErrors' # raw errors?
+EXIT_OPT = 'exitOnError' # exit on error?
+FLATTEN_OPT = 'flatten' # flatten pseudomodule namespace?
+OVERRIDE_OPT = 'override' # override sys.stdout with proxy?
+CALLBACK_OPT = 'noCallbackError' # is no custom callback an error?
+
+# Usage info.
+OPTION_INFO = [
+("-V --version", "Print version and exit"),
+("-h --help", "Print usage and exit"),
+("-H --extended-help", "Print extended usage and exit"),
+("-k --suppress-errors", "Do not exit on errors; go interactive"),
+("-p --prefix=<char>", "Change prefix to something other than @"),
+(" --no-prefix", "Do not do any markup processing at all"),
+("-m --module=<name>", "Change the internal pseudomodule name"),
+("-f --flatten", "Flatten the members of pseudmodule to start"),
+("-r --raw-errors", "Show raw Python errors"),
+("-i --interactive", "Go into interactive mode after processing"),
+("-n --no-override-stdout", "Do not override sys.stdout with proxy"),
+("-o --output=<filename>", "Specify file for output as write"),
+("-a --append=<filename>", "Specify file for output as append"),
+("-b --buffered-output", "Fully buffer output including open"),
+(" --binary", "Treat the file as a binary"),
+(" --chunk-size=<chunk>", "Use this chunk size for reading binaries"),
+("-P --preprocess=<filename>", "Interpret EmPy file before main processing"),
+("-I --import=<modules>", "Import Python modules before processing"),
+("-D --define=<definition>", "Execute Python assignment statement"),
+("-E --execute=<statement>", "Execute Python statement before processing"),
+("-F --execute-file=<filename>", "Execute Python file before processing"),
+(" --pause-at-end", "Prompt at the ending of processing"),
+(" --relative-path", "Add path of EmPy script to sys.path"),
+(" --no-callback-error", "Custom markup without callback is error"),
+(" --no-bangpath-processing", "Suppress bangpaths as comments"),
+("-u --unicode", "Enable Unicode subsystem (Python 2+ only)"),
+(" --unicode-encoding=<e>", "Set both input and output encodings"),
+(" --unicode-input-encoding=<e>", "Set input encoding"),
+(" --unicode-output-encoding=<e>", "Set output encoding"),
+(" --unicode-errors=<E>", "Set both input and output error handler"),
+(" --unicode-input-errors=<E>", "Set input error handler"),
+(" --unicode-output-errors=<E>", "Set output error handler"),
+]
+
+USAGE_NOTES = """\
+Notes: Whitespace immediately inside parentheses of @(...) are
+ignored. Whitespace immediately inside braces of @{...} are ignored,
+unless ... spans multiple lines. Use @{ ... }@ to suppress newline
+following expansion. Simple expressions ignore trailing dots; `@x.'
+means `@(x).'. A #! at the start of a file is treated as a @#
+comment."""
+
+MARKUP_INFO = [
+("@# ... NL", "Comment; remove everything up to newline"),
+("@? NAME NL", "Set the current context name"),
+("@! INTEGER NL", "Set the current context line number"),
+("@ WHITESPACE", "Remove following whitespace; line continuation"),
+("@\\ ESCAPE_CODE", "A C-style escape sequence"),
+("@@", "Literal @; @ is escaped (duplicated prefix)"),
+("@), @], @}", "Literal close parenthesis, bracket, brace"),
+("@ STRING_LITERAL", "Replace with string literal contents"),
+("@( EXPRESSION )", "Evaluate expression and substitute with str"),
+("@( TEST [? THEN [! ELSE]] )", "If test is true, evaluate then, otherwise else"),
+("@( TRY $ CATCH )", "Expand try expression, or catch if it raises"),
+("@ SIMPLE_EXPRESSION", "Evaluate simple expression and substitute;\n"
+ "e.g., @x, @x.y, @f(a, b), @l[i], etc."),
+("@` EXPRESSION `", "Evaluate expression and substitute with repr"),
+("@: EXPRESSION : [DUMMY] :", "Evaluates to @:...:expansion:"),
+("@{ STATEMENTS }", "Statements are executed for side effects"),
+("@[ CONTROL ]", "Control markups: if E; elif E; for N in E;\n"
+ "while E; try; except E, N; finally; continue;\n"
+ "break; end X"),
+("@%% KEY WHITESPACE VALUE NL", "Significator form of __KEY__ = VALUE"),
+("@< CONTENTS >", "Custom markup; meaning provided by user"),
+]
+
+ESCAPE_INFO = [
+("@\\0", "NUL, null"),
+("@\\a", "BEL, bell"),
+("@\\b", "BS, backspace"),
+("@\\dDDD", "three-digit decimal code DDD"),
+("@\\e", "ESC, escape"),
+("@\\f", "FF, form feed"),
+("@\\h", "DEL, delete"),
+("@\\n", "LF, linefeed, newline"),
+("@\\N{NAME}", "Unicode character named NAME"),
+("@\\oOOO", "three-digit octal code OOO"),
+("@\\qQQQQ", "four-digit quaternary code QQQQ"),
+("@\\r", "CR, carriage return"),
+("@\\s", "SP, space"),
+("@\\t", "HT, horizontal tab"),
+("@\\uHHHH", "16-bit hexadecimal Unicode HHHH"),
+("@\\UHHHHHHHH", "32-bit hexadecimal Unicode HHHHHHHH"),
+("@\\v", "VT, vertical tab"),
+("@\\xHH", "two-digit hexadecimal code HH"),
+("@\\z", "EOT, end of transmission"),
+]
+
+PSEUDOMODULE_INFO = [
+("VERSION", "String representing EmPy version"),
+("SIGNIFICATOR_RE_STRING", "Regular expression matching significators"),
+("SIGNIFICATOR_RE_SUFFIX", "The above stub, lacking the prefix"),
+("interpreter", "Currently-executing interpreter instance"),
+("argv", "The EmPy script name and command line arguments"),
+("args", "The command line arguments only"),
+("identify()", "Identify top context as name, line"),
+("setContextName(name)", "Set the name of the current context"),
+("setContextLine(line)", "Set the line number of the current context"),
+("atExit(callable)", "Invoke no-argument function at shutdown"),
+("getGlobals()", "Retrieve this interpreter's globals"),
+("setGlobals(dict)", "Set this interpreter's globals"),
+("updateGlobals(dict)", "Merge dictionary into interpreter's globals"),
+("clearGlobals()", "Start globals over anew"),
+("saveGlobals([deep])", "Save a copy of the globals"),
+("restoreGlobals([pop])", "Restore the most recently saved globals"),
+("defined(name, [loc])", "Find if the name is defined"),
+("evaluate(expression, [loc])", "Evaluate the expression"),
+("serialize(expression, [loc])", "Evaluate and serialize the expression"),
+("execute(statements, [loc])", "Execute the statements"),
+("single(source, [loc])", "Execute the 'single' object"),
+("atomic(name, value, [loc])", "Perform an atomic assignment"),
+("assign(name, value, [loc])", "Perform an arbitrary assignment"),
+("significate(key, [value])", "Significate the given key, value pair"),
+("include(file, [loc])", "Include filename or file-like object"),
+("expand(string, [loc])", "Explicitly expand string and return"),
+("string(data, [name], [loc])", "Process string-like object"),
+("quote(string)", "Quote prefixes in provided string and return"),
+("flatten([keys])", "Flatten module contents into globals namespace"),
+("getPrefix()", "Get current prefix"),
+("setPrefix(char)", "Set new prefix"),
+("stopDiverting()", "Stop diverting; data sent directly to output"),
+("createDiversion(name)", "Create a diversion but do not divert to it"),
+("retrieveDiversion(name)", "Retrieve the actual named diversion object"),
+("startDiversion(name)", "Start diverting to given diversion"),
+("playDiversion(name)", "Recall diversion and then eliminate it"),
+("replayDiversion(name)", "Recall diversion but retain it"),
+("purgeDiversion(name)", "Erase diversion"),
+("playAllDiversions()", "Stop diverting and play all diversions in order"),
+("replayAllDiversions()", "Stop diverting and replay all diversions"),
+("purgeAllDiversions()", "Stop diverting and purge all diversions"),
+("getFilter()", "Get current filter"),
+("resetFilter()", "Reset filter; no filtering"),
+("nullFilter()", "Install null filter"),
+("setFilter(shortcut)", "Install new filter or filter chain"),
+("attachFilter(shortcut)", "Attach single filter to end of current chain"),
+("areHooksEnabled()", "Return whether or not hooks are enabled"),
+("enableHooks()", "Enable hooks (default)"),
+("disableHooks()", "Disable hook invocation"),
+("getHooks()", "Get all the hooks"),
+("clearHooks()", "Clear all hooks"),
+("addHook(hook, [i])", "Register the hook (optionally insert)"),
+("removeHook(hook)", "Remove an already-registered hook from name"),
+("invokeHook(name_, ...)", "Manually invoke hook"),
+("getCallback()", "Get interpreter callback"),
+("registerCallback(callback)", "Register callback with interpreter"),
+("deregisterCallback()", "Deregister callback from interpreter"),
+("invokeCallback(contents)", "Invoke the callback directly"),
+("Interpreter", "The interpreter class"),
+]
+
+ENVIRONMENT_INFO = [
+(OPTIONS_ENV, "Specified options will be included"),
+(PREFIX_ENV, "Specify the default prefix: -p <value>"),
+(PSEUDO_ENV, "Specify name of pseudomodule: -m <value>"),
+(FLATTEN_ENV, "Flatten empy pseudomodule if defined: -f"),
+(RAW_ENV, "Show raw errors if defined: -r"),
+(INTERACTIVE_ENV, "Enter interactive mode if defined: -i"),
+(BUFFERED_ENV, "Fully buffered output if defined: -b"),
+(NO_OVERRIDE_ENV, "Do not override sys.stdout if defined: -n"),
+(UNICODE_ENV, "Enable Unicode subsystem: -n"),
+(INPUT_ENCODING_ENV, "Unicode input encoding"),
+(OUTPUT_ENCODING_ENV, "Unicode output encoding"),
+(INPUT_ERRORS_ENV, "Unicode input error handler"),
+(OUTPUT_ERRORS_ENV, "Unicode output error handler"),
+]
+
+class Error(Exception):
+ """The base class for all EmPy errors."""
+ pass
+
+EmpyError = EmPyError = Error # DEPRECATED
+
+class DiversionError(Error):
+ """An error related to diversions."""
+ pass
+
+class FilterError(Error):
+ """An error related to filters."""
+ pass
+
+class StackUnderflowError(Error):
+ """A stack underflow."""
+ pass
+
+class SubsystemError(Error):
+ """An error associated with the Unicode subsystem."""
+ pass
+
+class FlowError(Error):
+ """An exception related to control flow."""
+ pass
+
+class ContinueFlow(FlowError):
+ """A continue control flow."""
+ pass
+
+class BreakFlow(FlowError):
+ """A break control flow."""
+ pass
+
+class ParseError(Error):
+ """A parse error occurred."""
+ pass
+
+class TransientParseError(ParseError):
+ """A parse error occurred which may be resolved by feeding more data.
+ Such an error reaching the toplevel is an unexpected EOF error."""
+ pass
+
+
+class MetaError(Exception):
+
+ """A wrapper around a real Python exception for including a copy of
+ the context."""
+
+ def __init__(self, contexts, exc):
+ Exception.__init__(self, exc)
+ self.contexts = contexts
+ self.exc = exc
+
+ def __str__(self):
+ backtrace = map(lambda x: str(x), self.contexts)
+ return "%s: %s (%s)" % (self.exc.__class__, self.exc, \
+ (string.join(backtrace, ', ')))
+
+
+class Subsystem:
+
+ """The subsystem class defers file creation so that it can create
+ Unicode-wrapped files if desired (and possible)."""
+
+ def __init__(self):
+ self.useUnicode = False
+ self.inputEncoding = None
+ self.outputEncoding = None
+ self.errors = None
+
+ def initialize(self, inputEncoding=None, outputEncoding=None, \
+ inputErrors=None, outputErrors=None):
+ self.useUnicode = True
+ try:
+ unicode
+ import codecs
+ except (NameError, ImportError):
+ raise SubsystemError, "Unicode subsystem unavailable"
+ defaultEncoding = sys.getdefaultencoding()
+ if inputEncoding is None:
+ inputEncoding = defaultEncoding
+ self.inputEncoding = inputEncoding
+ if outputEncoding is None:
+ outputEncoding = defaultEncoding
+ self.outputEncoding = outputEncoding
+ if inputErrors is None:
+ inputErrors = DEFAULT_ERRORS
+ self.inputErrors = inputErrors
+ if outputErrors is None:
+ outputErrors = DEFAULT_ERRORS
+ self.outputErrors = outputErrors
+
+ def assertUnicode(self):
+ if not self.useUnicode:
+ raise SubsystemError, "Unicode subsystem unavailable"
+
+ def open(self, name, mode=None):
+ if self.useUnicode:
+ return self.unicodeOpen(name, mode)
+ else:
+ return self.defaultOpen(name, mode)
+
+ def defaultOpen(self, name, mode=None):
+ if mode is None:
+ mode = 'r'
+ return open(name, mode)
+
+ def unicodeOpen(self, name, mode=None):
+ import codecs
+ if mode is None:
+ mode = 'rb'
+ if mode.find('w') >= 0 or mode.find('a') >= 0:
+ encoding = self.outputEncoding
+ errors = self.outputErrors
+ else:
+ encoding = self.inputEncoding
+ errors = self.inputErrors
+ return codecs.open(name, mode, encoding, errors)
+
+theSubsystem = Subsystem()
+
+
+class Stack:
+
+ """A simple stack that behaves as a sequence (with 0 being the top
+ of the stack, not the bottom)."""
+
+ def __init__(self, seq=None):
+ if seq is None:
+ seq = []
+ self.data = seq
+
+ def top(self):
+ """Access the top element on the stack."""
+ try:
+ return self.data[-1]
+ except IndexError:
+ raise StackUnderflowError, "stack is empty for top"
+
+ def pop(self):
+ """Pop the top element off the stack and return it."""
+ try:
+ return self.data.pop()
+ except IndexError:
+ raise StackUnderflowError, "stack is empty for pop"
+
+ def push(self, object):
+ """Push an element onto the top of the stack."""
+ self.data.append(object)
+
+ def filter(self, function):
+ """Filter the elements of the stack through the function."""
+ self.data = filter(function, self.data)
+
+ def purge(self):
+ """Purge the stack."""
+ self.data = []
+
+ def clone(self):
+ """Create a duplicate of this stack."""
+ return self.__class__(self.data[:])
+
+ def __nonzero__(self): return len(self.data) != 0
+ def __len__(self): return len(self.data)
+ def __getitem__(self, index): return self.data[-(index + 1)]
+
+ def __repr__(self):
+ return '<%s instance at 0x%x [%s]>' % \
+ (self.__class__, id(self), \
+ string.join(map(repr, self.data), ', '))
+
+
+class AbstractFile:
+
+ """An abstracted file that, when buffered, will totally buffer the
+ file, including even the file open."""
+
+ def __init__(self, filename, mode='w', buffered=False):
+ # The calls below might throw, so start off by marking this
+ # file as "done." This way destruction of a not-completely-
+ # initialized AbstractFile will generate no further errors.
+ self.done = True
+ self.filename = filename
+ self.mode = mode
+ self.buffered = buffered
+ if buffered:
+ self.bufferFile = StringIO.StringIO()
+ else:
+ self.bufferFile = theSubsystem.open(filename, mode)
+ # Okay, we got this far, so the AbstractFile is initialized.
+ # Flag it as "not done."
+ self.done = False
+
+ def __del__(self):
+ self.close()
+
+ def write(self, data):
+ self.bufferFile.write(data)
+
+ def writelines(self, data):
+ self.bufferFile.writelines(data)
+
+ def flush(self):
+ self.bufferFile.flush()
+
+ def close(self):
+ if not self.done:
+ self.commit()
+ self.done = True
+
+ def commit(self):
+ if self.buffered:
+ file = theSubsystem.open(self.filename, self.mode)
+ file.write(self.bufferFile.getvalue())
+ file.close()
+ else:
+ self.bufferFile.close()
+
+ def abort(self):
+ if self.buffered:
+ self.bufferFile = None
+ else:
+ self.bufferFile.close()
+ self.bufferFile = None
+ self.done = True
+
+
+class Diversion:
+
+ """The representation of an active diversion. Diversions act as
+ (writable) file objects, and then can be recalled either as pure
+ strings or (readable) file objects."""
+
+ def __init__(self):
+ self.file = StringIO.StringIO()
+
+ # These methods define the writable file-like interface for the
+ # diversion.
+
+ def write(self, data):
+ self.file.write(data)
+
+ def writelines(self, lines):
+ for line in lines:
+ self.write(line)
+
+ def flush(self):
+ self.file.flush()
+
+ def close(self):
+ self.file.close()
+
+ # These methods are specific to diversions.
+
+ def asString(self):
+ """Return the diversion as a string."""
+ return self.file.getvalue()
+
+ def asFile(self):
+ """Return the diversion as a file."""
+ return StringIO.StringIO(self.file.getvalue())
+
+
+class Stream:
+
+ """A wrapper around an (output) file object which supports
+ diversions and filtering."""
+
+ def __init__(self, file):
+ self.file = file
+ self.currentDiversion = None
+ self.diversions = {}
+ self.filter = file
+ self.done = False
+
+ def write(self, data):
+ if self.currentDiversion is None:
+ self.filter.write(data)
+ else:
+ self.diversions[self.currentDiversion].write(data)
+
+ def writelines(self, lines):
+ for line in lines:
+ self.write(line)
+
+ def flush(self):
+ self.filter.flush()
+
+ def close(self):
+ if not self.done:
+ self.undivertAll(True)
+ self.filter.close()
+ self.done = True
+
+ def shortcut(self, shortcut):
+ """Take a filter shortcut and translate it into a filter, returning
+ it. Sequences don't count here; these should be detected
+ independently."""
+ if shortcut == 0:
+ return NullFilter()
+ elif type(shortcut) is types.FunctionType or \
+ type(shortcut) is types.BuiltinFunctionType or \
+ type(shortcut) is types.BuiltinMethodType or \
+ type(shortcut) is types.LambdaType:
+ return FunctionFilter(shortcut)
+ elif type(shortcut) is types.StringType:
+ return StringFilter(filter)
+ elif type(shortcut) is types.DictType:
+ raise NotImplementedError, "mapping filters not yet supported"
+ else:
+ # Presume it's a plain old filter.
+ return shortcut
+
+ def last(self):
+ """Find the last filter in the current filter chain, or None if
+ there are no filters installed."""
+ if self.filter is None:
+ return None
+ thisFilter, lastFilter = self.filter, None
+ while thisFilter is not None and thisFilter is not self.file:
+ lastFilter = thisFilter
+ thisFilter = thisFilter.next()
+ return lastFilter
+
+ def install(self, shortcut=None):
+ """Install a new filter; None means no filter. Handle all the
+ special shortcuts for filters here."""
+ # Before starting, execute a flush.
+ self.filter.flush()
+ if shortcut is None or shortcut == [] or shortcut == ():
+ # Shortcuts for "no filter."
+ self.filter = self.file
+ else:
+ if type(shortcut) in (types.ListType, types.TupleType):
+ shortcuts = list(shortcut)
+ else:
+ shortcuts = [shortcut]
+ # Run through the shortcut filter names, replacing them with
+ # full-fledged instances of Filter.
+ filters = []
+ for shortcut in shortcuts:
+ filters.append(self.shortcut(shortcut))
+ if len(filters) > 1:
+ # If there's more than one filter provided, chain them
+ # together.
+ lastFilter = None
+ for filter in filters:
+ if lastFilter is not None:
+ lastFilter.attach(filter)
+ lastFilter = filter
+ lastFilter.attach(self.file)
+ self.filter = filters[0]
+ else:
+ # If there's only one filter, assume that it's alone or it's
+ # part of a chain that has already been manually chained;
+ # just find the end.
+ filter = filters[0]
+ lastFilter = filter.last()
+ lastFilter.attach(self.file)
+ self.filter = filter
+
+ def attach(self, shortcut):
+ """Attached a solitary filter (no sequences allowed here) at the
+ end of the current filter chain."""
+ lastFilter = self.last()
+ if lastFilter is None:
+ # Just install it from scratch if there is no active filter.
+ self.install(shortcut)
+ else:
+ # Attach the last filter to this one, and this one to the file.
+ filter = self.shortcut(shortcut)
+ lastFilter.attach(filter)
+ filter.attach(self.file)
+
+ def revert(self):
+ """Reset any current diversions."""
+ self.currentDiversion = None
+
+ def create(self, name):
+ """Create a diversion if one does not already exist, but do not
+ divert to it yet."""
+ if name is None:
+ raise DiversionError, "diversion name must be non-None"
+ if not self.diversions.has_key(name):
+ self.diversions[name] = Diversion()
+
+ def retrieve(self, name):
+ """Retrieve the given diversion."""
+ if name is None:
+ raise DiversionError, "diversion name must be non-None"
+ if self.diversions.has_key(name):
+ return self.diversions[name]
+ else:
+ raise DiversionError, "nonexistent diversion: %s" % name
+
+ def divert(self, name):
+ """Start diverting."""
+ if name is None:
+ raise DiversionError, "diversion name must be non-None"
+ self.create(name)
+ self.currentDiversion = name
+
+ def undivert(self, name, purgeAfterwards=False):
+ """Undivert a particular diversion."""
+ if name is None:
+ raise DiversionError, "diversion name must be non-None"
+ if self.diversions.has_key(name):
+ diversion = self.diversions[name]
+ self.filter.write(diversion.asString())
+ if purgeAfterwards:
+ self.purge(name)
+ else:
+ raise DiversionError, "nonexistent diversion: %s" % name
+
+ def purge(self, name):
+ """Purge the specified diversion."""
+ if name is None:
+ raise DiversionError, "diversion name must be non-None"
+ if self.diversions.has_key(name):
+ del self.diversions[name]
+ if self.currentDiversion == name:
+ self.currentDiversion = None
+
+ def undivertAll(self, purgeAfterwards=True):
+ """Undivert all pending diversions."""
+ if self.diversions:
+ self.revert() # revert before undiverting!
+ names = self.diversions.keys()
+ names.sort()
+ for name in names:
+ self.undivert(name)
+ if purgeAfterwards:
+ self.purge(name)
+
+ def purgeAll(self):
+ """Eliminate all existing diversions."""
+ if self.diversions:
+ self.diversions = {}
+ self.currentDiversion = None
+
+
+class NullFile:
+
+ """A simple class that supports all the file-like object methods
+ but simply does nothing at all."""
+
+ def __init__(self): pass
+ def write(self, data): pass
+ def writelines(self, lines): pass
+ def flush(self): pass
+ def close(self): pass
+
+
+class UncloseableFile:
+
+ """A simple class which wraps around a delegate file-like object
+ and lets everything through except close calls."""
+
+ def __init__(self, delegate):
+ self.delegate = delegate
+
+ def write(self, data):
+ self.delegate.write(data)
+
+ def writelines(self, lines):
+ self.delegate.writelines(data)
+
+ def flush(self):
+ self.delegate.flush()
+
+ def close(self):
+ """Eat this one."""
+ pass
+
+
+class ProxyFile:
+
+ """The proxy file object that is intended to take the place of
+ sys.stdout. The proxy can manage a stack of file objects it is
+ writing to, and an underlying raw file object."""
+
+ def __init__(self, bottom):
+ self.stack = Stack()
+ self.bottom = bottom
+
+ def current(self):
+ """Get the current stream to write to."""
+ if self.stack:
+ return self.stack[-1][1]
+ else:
+ return self.bottom
+
+ def push(self, interpreter):
+ self.stack.push((interpreter, interpreter.stream()))
+
+ def pop(self, interpreter):
+ result = self.stack.pop()
+ assert interpreter is result[0]
+
+ def clear(self, interpreter):
+ self.stack.filter(lambda x, i=interpreter: x[0] is not i)
+
+ def write(self, data):
+ self.current().write(data)
+
+ def writelines(self, lines):
+ self.current().writelines(lines)
+
+ def flush(self):
+ self.current().flush()
+
+ def close(self):
+ """Close the current file. If the current file is the bottom, then
+ close it and dispose of it."""
+ current = self.current()
+ if current is self.bottom:
+ self.bottom = None
+ current.close()
+
+ def _testProxy(self): pass
+
+
+class Filter:
+
+ """An abstract filter."""
+
+ def __init__(self):
+ if self.__class__ is Filter:
+ raise NotImplementedError
+ self.sink = None
+
+ def next(self):
+ """Return the next filter/file-like object in the sequence, or None."""
+ return self.sink
+
+ def write(self, data):
+ """The standard write method; this must be overridden in subclasses."""
+ raise NotImplementedError
+
+ def writelines(self, lines):
+ """Standard writelines wrapper."""
+ for line in lines:
+ self.write(line)
+
+ def _flush(self):
+ """The _flush method should always flush the sink and should not
+ be overridden."""
+ self.sink.flush()
+
+ def flush(self):
+ """The flush method can be overridden."""
+ self._flush()
+
+ def close(self):
+ """Close the filter. Do an explicit flush first, then close the
+ sink."""
+ self.flush()
+ self.sink.close()
+
+ def attach(self, filter):
+ """Attach a filter to this one."""
+ if self.sink is not None:
+ # If it's already attached, detach it first.
+ self.detach()
+ self.sink = filter
+
+ def detach(self):
+ """Detach a filter from its sink."""
+ self.flush()
+ self._flush() # do a guaranteed flush to just to be safe
+ self.sink = None
+
+ def last(self):
+ """Find the last filter in this chain."""
+ this, last = self, self
+ while this is not None:
+ last = this
+ this = this.next()
+ return last
+
+class NullFilter(Filter):
+
+ """A filter that never sends any output to its sink."""
+
+ def write(self, data): pass
+
+class FunctionFilter(Filter):
+
+ """A filter that works simply by pumping its input through a
+ function which maps strings into strings."""
+
+ def __init__(self, function):
+ Filter.__init__(self)
+ self.function = function
+
+ def write(self, data):
+ self.sink.write(self.function(data))
+
+class StringFilter(Filter):
+
+ """A filter that takes a translation string (256 characters) and
+ filters any incoming data through it."""
+
+ def __init__(self, table):
+ if not (type(table) == types.StringType and len(table) == 256):
+ raise FilterError, "table must be 256-character string"
+ Filter.__init__(self)
+ self.table = table
+
+ def write(self, data):
+ self.sink.write(string.translate(data, self.table))
+
+class BufferedFilter(Filter):
+
+ """A buffered filter is one that doesn't modify the source data
+ sent to the sink, but instead holds it for a time. The standard
+ variety only sends the data along when it receives a flush
+ command."""
+
+ def __init__(self):
+ Filter.__init__(self)
+ self.buffer = ''
+
+ def write(self, data):
+ self.buffer = self.buffer + data
+
+ def flush(self):
+ if self.buffer:
+ self.sink.write(self.buffer)
+ self._flush()
+
+class SizeBufferedFilter(BufferedFilter):
+
+ """A size-buffered filter only in fixed size chunks (excepting the
+ final chunk)."""
+
+ def __init__(self, bufferSize):
+ BufferedFilter.__init__(self)
+ self.bufferSize = bufferSize
+
+ def write(self, data):
+ BufferedFilter.write(self, data)
+ while len(self.buffer) > self.bufferSize:
+ chunk, self.buffer = \
+ self.buffer[:self.bufferSize], self.buffer[self.bufferSize:]
+ self.sink.write(chunk)
+
+class LineBufferedFilter(BufferedFilter):
+
+ """A line-buffered filter only lets data through when it sees
+ whole lines."""
+
+ def __init__(self):
+ BufferedFilter.__init__(self)
+
+ def write(self, data):
+ BufferedFilter.write(self, data)
+ chunks = string.split(self.buffer, '\n')
+ for chunk in chunks[:-1]:
+ self.sink.write(chunk + '\n')
+ self.buffer = chunks[-1]
+
+class MaximallyBufferedFilter(BufferedFilter):
+
+ """A maximally-buffered filter only lets its data through on the final
+ close. It ignores flushes."""
+
+ def __init__(self):
+ BufferedFilter.__init__(self)
+
+ def flush(self): pass
+
+ def close(self):
+ if self.buffer:
+ BufferedFilter.flush(self)
+ self.sink.close()
+
+
+class Context:
+
+ """An interpreter context, which encapsulates a name, an input
+ file object, and a parser object."""
+
+ DEFAULT_UNIT = 'lines'
+
+ def __init__(self, name, line=0, units=DEFAULT_UNIT):
+ self.name = name
+ self.line = line
+ self.units = units
+ self.pause = False
+
+ def bump(self, quantity=1):
+ if self.pause:
+ self.pause = False
+ else:
+ self.line = self.line + quantity
+
+ def identify(self):
+ return self.name, self.line
+
+ def __str__(self):
+ if self.units == self.DEFAULT_UNIT:
+ return "%s:%s" % (self.name, self.line)
+ else:
+ return "%s:%s[%s]" % (self.name, self.line, self.units)
+
+
+class Hook:
+
+ """The base class for implementing hooks."""
+
+ def __init__(self):
+ self.interpreter = None
+
+ def register(self, interpreter):
+ self.interpreter = interpreter
+
+ def deregister(self, interpreter):
+ if interpreter is not self.interpreter:
+ raise Error, "hook not associated with this interpreter"
+ self.interpreter = None
+
+ def push(self):
+ self.interpreter.push()
+
+ def pop(self):
+ self.interpreter.pop()
+
+ def null(self): pass
+
+ def atStartup(self): pass
+ def atReady(self): pass
+ def atFinalize(self): pass
+ def atShutdown(self): pass
+ def atParse(self, scanner, locals): pass
+ def atToken(self, token): pass
+ def atHandle(self, meta): pass
+ def atInteract(self): pass
+
+ def beforeInclude(self, name, file, locals): pass
+ def afterInclude(self): pass
+
+ def beforeExpand(self, string, locals): pass
+ def afterExpand(self, result): pass
+
+ def beforeFile(self, name, file, locals): pass
+ def afterFile(self): pass
+
+ def beforeBinary(self, name, file, chunkSize, locals): pass
+ def afterBinary(self): pass
+
+ def beforeString(self, name, string, locals): pass
+ def afterString(self): pass
+
+ def beforeQuote(self, string): pass
+ def afterQuote(self, result): pass
+
+ def beforeEscape(self, string, more): pass
+ def afterEscape(self, result): pass
+
+ def beforeControl(self, type, rest, locals): pass
+ def afterControl(self): pass
+
+ def beforeSignificate(self, key, value, locals): pass
+ def afterSignificate(self): pass
+
+ def beforeAtomic(self, name, value, locals): pass
+ def afterAtomic(self): pass
+
+ def beforeMulti(self, name, values, locals): pass
+ def afterMulti(self): pass
+
+ def beforeImport(self, name, locals): pass
+ def afterImport(self): pass
+
+ def beforeClause(self, catch, locals): pass
+ def afterClause(self, exception, variable): pass
+
+ def beforeSerialize(self, expression, locals): pass
+ def afterSerialize(self): pass
+
+ def beforeDefined(self, name, locals): pass
+ def afterDefined(self, result): pass
+
+ def beforeLiteral(self, text): pass
+ def afterLiteral(self): pass
+
+ def beforeEvaluate(self, expression, locals): pass
+ def afterEvaluate(self, result): pass
+
+ def beforeExecute(self, statements, locals): pass
+ def afterExecute(self): pass
+
+ def beforeSingle(self, source, locals): pass
+ def afterSingle(self): pass
+
+class VerboseHook(Hook):
+
+ """A verbose hook that reports all information received by the
+ hook interface. This class dynamically scans the Hook base class
+ to ensure that all hook methods are properly represented."""
+
+ EXEMPT_ATTRIBUTES = ['register', 'deregister', 'push', 'pop']
+
+ def __init__(self, output=sys.stderr):
+ Hook.__init__(self)
+ self.output = output
+ self.indent = 0
+
+ class FakeMethod:
+ """This is a proxy method-like object."""
+ def __init__(self, hook, name):
+ self.hook = hook
+ self.name = name
+
+ def __call__(self, **keywords):
+ self.hook.output.write("%s%s: %s\n" % \
+ (' ' * self.hook.indent, \
+ self.name, repr(keywords)))
+
+ for attribute in dir(Hook):
+ if attribute[:1] != '_' and \
+ attribute not in self.EXEMPT_ATTRIBUTES:
+ self.__dict__[attribute] = FakeMethod(self, attribute)
+
+
+class Token:
+
+ """An element of expansion."""
+
+ def run(self, interpreter, locals):
+ raise NotImplementedError
+
+ def string(self):
+ raise NotImplementedError
+
+ def __str__(self): return self.string()
+
+class NullToken(Token):
+ """A chunk of data not containing markups."""
+ def __init__(self, data):
+ self.data = data
+
+ def run(self, interpreter, locals):
+ interpreter.write(self.data)
+
+ def string(self):
+ return self.data
+
+class ExpansionToken(Token):
+ """A token that involves an expansion."""
+ def __init__(self, prefix, first):
+ self.prefix = prefix
+ self.first = first
+
+ def scan(self, scanner):
+ pass
+
+ def run(self, interpreter, locals):
+ pass
+
+class WhitespaceToken(ExpansionToken):
+ """A whitespace markup."""
+ def string(self):
+ return '%s%s' % (self.prefix, self.first)
+
+class LiteralToken(ExpansionToken):
+ """A literal markup."""
+ def run(self, interpreter, locals):
+ interpreter.write(self.first)
+
+ def string(self):
+ return '%s%s' % (self.prefix, self.first)
+
+class PrefixToken(ExpansionToken):
+ """A prefix markup."""
+ def run(self, interpreter, locals):
+ interpreter.write(interpreter.prefix)
+
+ def string(self):
+ return self.prefix * 2
+
+class CommentToken(ExpansionToken):
+ """A comment markup."""
+ def scan(self, scanner):
+ loc = scanner.find('\n')
+ if loc >= 0:
+ self.comment = scanner.chop(loc, 1)
+ else:
+ raise TransientParseError, "comment expects newline"
+
+ def string(self):
+ return '%s#%s\n' % (self.prefix, self.comment)
+
+class ContextNameToken(ExpansionToken):
+ """A context name change markup."""
+ def scan(self, scanner):
+ loc = scanner.find('\n')
+ if loc >= 0:
+ self.name = string.strip(scanner.chop(loc, 1))
+ else:
+ raise TransientParseError, "context name expects newline"
+
+ def run(self, interpreter, locals):
+ context = interpreter.context()
+ context.name = self.name
+
+class ContextLineToken(ExpansionToken):
+ """A context line change markup."""
+ def scan(self, scanner):
+ loc = scanner.find('\n')
+ if loc >= 0:
+ try:
+ self.line = int(scanner.chop(loc, 1))
+ except ValueError:
+ raise ParseError, "context line requires integer"
+ else:
+ raise TransientParseError, "context line expects newline"
+
+ def run(self, interpreter, locals):
+ context = interpreter.context()
+ context.line = self.line
+ context.pause = True
+
+class EscapeToken(ExpansionToken):
+ """An escape markup."""
+ def scan(self, scanner):
+ try:
+ code = scanner.chop(1)
+ result = None
+ if code in '()[]{}\'\"\\': # literals
+ result = code
+ elif code == '0': # NUL
+ result = '\x00'
+ elif code == 'a': # BEL
+ result = '\x07'
+ elif code == 'b': # BS
+ result = '\x08'
+ elif code == 'd': # decimal code
+ decimalCode = scanner.chop(3)
+ result = chr(string.atoi(decimalCode, 10))
+ elif code == 'e': # ESC
+ result = '\x1b'
+ elif code == 'f': # FF
+ result = '\x0c'
+ elif code == 'h': # DEL
+ result = '\x7f'
+ elif code == 'n': # LF (newline)
+ result = '\x0a'
+ elif code == 'N': # Unicode character name
+ theSubsystem.assertUnicode()
+ import unicodedata
+ if scanner.chop(1) != '{':
+ raise ParseError, r"Unicode name escape should be \N{...}"
+ i = scanner.find('}')
+ name = scanner.chop(i, 1)
+ try:
+ result = unicodedata.lookup(name)
+ except KeyError:
+ raise SubsystemError, \
+ "unknown Unicode character name: %s" % name
+ elif code == 'o': # octal code
+ octalCode = scanner.chop(3)
+ result = chr(string.atoi(octalCode, 8))
+ elif code == 'q': # quaternary code
+ quaternaryCode = scanner.chop(4)
+ result = chr(string.atoi(quaternaryCode, 4))
+ elif code == 'r': # CR
+ result = '\x0d'
+ elif code in 's ': # SP
+ result = ' '
+ elif code == 't': # HT
+ result = '\x09'
+ elif code in 'u': # Unicode 16-bit hex literal
+ theSubsystem.assertUnicode()
+ hexCode = scanner.chop(4)
+ result = unichr(string.atoi(hexCode, 16))
+ elif code in 'U': # Unicode 32-bit hex literal
+ theSubsystem.assertUnicode()
+ hexCode = scanner.chop(8)
+ result = unichr(string.atoi(hexCode, 16))
+ elif code == 'v': # VT
+ result = '\x0b'
+ elif code == 'x': # hexadecimal code
+ hexCode = scanner.chop(2)
+ result = chr(string.atoi(hexCode, 16))
+ elif code == 'z': # EOT
+ result = '\x04'
+ elif code == '^': # control character
+ controlCode = string.upper(scanner.chop(1))
+ if controlCode >= '@' and controlCode <= '`':
+ result = chr(ord(controlCode) - ord('@'))
+ elif controlCode == '?':
+ result = '\x7f'
+ else:
+ raise ParseError, "invalid escape control code"
+ else:
+ raise ParseError, "unrecognized escape code"
+ assert result is not None
+ self.code = result
+ except ValueError:
+ raise ParseError, "invalid numeric escape code"
+
+ def run(self, interpreter, locals):
+ interpreter.write(self.code)
+
+ def string(self):
+ return '%s\\x%02x' % (self.prefix, ord(self.code))
+
+class SignificatorToken(ExpansionToken):
+ """A significator markup."""
+ def scan(self, scanner):
+ loc = scanner.find('\n')
+ if loc >= 0:
+ line = scanner.chop(loc, 1)
+ if not line:
+ raise ParseError, "significator must have nonblank key"
+ if line[0] in ' \t\v\n':
+ raise ParseError, "no whitespace between % and key"
+ # Work around a subtle CPython-Jython difference by stripping
+ # the string before splitting it: 'a '.split(None, 1) has two
+ # elements in Jython 2.1).
+ fields = string.split(string.strip(line), None, 1)
+ if len(fields) == 2 and fields[1] == '':
+ fields.pop()
+ self.key = fields[0]
+ if len(fields) < 2:
+ fields.append(None)
+ self.key, self.valueCode = fields
+ else:
+ raise TransientParseError, "significator expects newline"
+
+ def run(self, interpreter, locals):
+ value = self.valueCode
+ if value is not None:
+ value = interpreter.evaluate(string.strip(value), locals)
+ interpreter.significate(self.key, value)
+
+ def string(self):
+ if self.valueCode is None:
+ return '%s%%%s\n' % (self.prefix, self.key)
+ else:
+ return '%s%%%s %s\n' % (self.prefix, self.key, self.valueCode)
+
+class ExpressionToken(ExpansionToken):
+ """An expression markup."""
+ def scan(self, scanner):
+ z = scanner.complex('(', ')', 0)
+ try:
+ q = scanner.next('$', 0, z, True)
+ except ParseError:
+ q = z
+ try:
+ i = scanner.next('?', 0, q, True)
+ try:
+ j = scanner.next('!', i, q, True)
+ except ParseError:
+ try:
+ j = scanner.next(':', i, q, True) # DEPRECATED
+ except ParseError:
+ j = q
+ except ParseError:
+ i = j = q
+ code = scanner.chop(z, 1)
+ self.testCode = code[:i]
+ self.thenCode = code[i + 1:j]
+ self.elseCode = code[j + 1:q]
+ self.exceptCode = code[q + 1:z]
+
+ def run(self, interpreter, locals):
+ try:
+ result = interpreter.evaluate(self.testCode, locals)
+ if self.thenCode:
+ if result:
+ result = interpreter.evaluate(self.thenCode, locals)
+ else:
+ if self.elseCode:
+ result = interpreter.evaluate(self.elseCode, locals)
+ else:
+ result = None
+ except SyntaxError:
+ # Don't catch syntax errors; let them through.
+ raise
+ except:
+ if self.exceptCode:
+ result = interpreter.evaluate(self.exceptCode, locals)
+ else:
+ raise
+ if result is not None:
+ interpreter.write(str(result))
+
+ def string(self):
+ result = self.testCode
+ if self.thenCode:
+ result = result + '?' + self.thenCode
+ if self.elseCode:
+ result = result + '!' + self.elseCode
+ if self.exceptCode:
+ result = result + '$' + self.exceptCode
+ return '%s(%s)' % (self.prefix, result)
+
+class StringLiteralToken(ExpansionToken):
+ """A string token markup."""
+ def scan(self, scanner):
+ scanner.retreat()
+ assert scanner[0] == self.first
+ i = scanner.quote()
+ self.literal = scanner.chop(i)
+
+ def run(self, interpreter, locals):
+ interpreter.literal(self.literal)
+
+ def string(self):
+ return '%s%s' % (self.prefix, self.literal)
+
+class SimpleExpressionToken(ExpansionToken):
+ """A simple expression markup."""
+ def scan(self, scanner):
+ i = scanner.simple()
+ self.code = self.first + scanner.chop(i)
+
+ def run(self, interpreter, locals):
+ interpreter.serialize(self.code, locals)
+
+ def string(self):
+ return '%s%s' % (self.prefix, self.code)
+
+class ReprToken(ExpansionToken):
+ """A repr markup."""
+ def scan(self, scanner):
+ i = scanner.next('`', 0)
+ self.code = scanner.chop(i, 1)
+
+ def run(self, interpreter, locals):
+ interpreter.write(repr(interpreter.evaluate(self.code, locals)))
+
+ def string(self):
+ return '%s`%s`' % (self.prefix, self.code)
+
+class InPlaceToken(ExpansionToken):
+ """An in-place markup."""
+ def scan(self, scanner):
+ i = scanner.next(':', 0)
+ j = scanner.next(':', i + 1)
+ self.code = scanner.chop(i, j - i + 1)
+
+ def run(self, interpreter, locals):
+ interpreter.write("%s:%s:" % (interpreter.prefix, self.code))
+ try:
+ interpreter.serialize(self.code, locals)
+ finally:
+ interpreter.write(":")
+
+ def string(self):
+ return '%s:%s::' % (self.prefix, self.code)
+
+class StatementToken(ExpansionToken):
+ """A statement markup."""
+ def scan(self, scanner):
+ i = scanner.complex('{', '}', 0)
+ self.code = scanner.chop(i, 1)
+
+ def run(self, interpreter, locals):
+ interpreter.execute(self.code, locals)
+
+ def string(self):
+ return '%s{%s}' % (self.prefix, self.code)
+
+class CustomToken(ExpansionToken):
+ """A custom markup."""
+ def scan(self, scanner):
+ i = scanner.complex('<', '>', 0)
+ self.contents = scanner.chop(i, 1)
+
+ def run(self, interpreter, locals):
+ interpreter.invokeCallback(self.contents)
+
+ def string(self):
+ return '%s<%s>' % (self.prefix, self.contents)
+
+class ControlToken(ExpansionToken):
+
+ """A control token."""
+
+ PRIMARY_TYPES = ['if', 'for', 'while', 'try', 'def']
+ SECONDARY_TYPES = ['elif', 'else', 'except', 'finally']
+ TERTIARY_TYPES = ['continue', 'break']
+ GREEDY_TYPES = ['if', 'elif', 'for', 'while', 'def', 'end']
+ END_TYPES = ['end']
+
+ IN_RE = re.compile(r"\bin\b")
+
+ def scan(self, scanner):
+ scanner.acquire()
+ i = scanner.complex('[', ']', 0)
+ self.contents = scanner.chop(i, 1)
+ fields = string.split(string.strip(self.contents), ' ', 1)
+ if len(fields) > 1:
+ self.type, self.rest = fields
+ else:
+ self.type = fields[0]
+ self.rest = None
+ self.subtokens = []
+ if self.type in self.GREEDY_TYPES and self.rest is None:
+ raise ParseError, "control '%s' needs arguments" % self.type
+ if self.type in self.PRIMARY_TYPES:
+ self.subscan(scanner, self.type)
+ self.kind = 'primary'
+ elif self.type in self.SECONDARY_TYPES:
+ self.kind = 'secondary'
+ elif self.type in self.TERTIARY_TYPES:
+ self.kind = 'tertiary'
+ elif self.type in self.END_TYPES:
+ self.kind = 'end'
+ else:
+ raise ParseError, "unknown control markup: '%s'" % self.type
+ scanner.release()
+
+ def subscan(self, scanner, primary):
+ """Do a subscan for contained tokens."""
+ while True:
+ token = scanner.one()
+ if token is None:
+ raise TransientParseError, \
+ "control '%s' needs more tokens" % primary
+ if isinstance(token, ControlToken) and \
+ token.type in self.END_TYPES:
+ if token.rest != primary:
+ raise ParseError, \
+ "control must end with 'end %s'" % primary
+ break
+ self.subtokens.append(token)
+
+ def build(self, allowed=None):
+ """Process the list of subtokens and divide it into a list of
+ 2-tuples, consisting of the dividing tokens and the list of
+ subtokens that follow them. If allowed is specified, it will
+ represent the list of the only secondary markup types which
+ are allowed."""
+ if allowed is None:
+ allowed = SECONDARY_TYPES
+ result = []
+ latest = []
+ result.append((self, latest))
+ for subtoken in self.subtokens:
+ if isinstance(subtoken, ControlToken) and \
+ subtoken.kind == 'secondary':
+ if subtoken.type not in allowed:
+ raise ParseError, \
+ "control unexpected secondary: '%s'" % subtoken.type
+ latest = []
+ result.append((subtoken, latest))
+ else:
+ latest.append(subtoken)
+ return result
+
+ def run(self, interpreter, locals):
+ interpreter.invoke('beforeControl', type=self.type, rest=self.rest, \
+ locals=locals)
+ if self.type == 'if':
+ info = self.build(['elif', 'else'])
+ elseTokens = None
+ if info[-1][0].type == 'else':
+ elseTokens = info.pop()[1]
+ for secondary, subtokens in info:
+ if secondary.type not in ('if', 'elif'):
+ raise ParseError, \
+ "control 'if' unexpected secondary: '%s'" % secondary.type
+ if interpreter.evaluate(secondary.rest, locals):
+ self.subrun(subtokens, interpreter, locals)
+ break
+ else:
+ if elseTokens:
+ self.subrun(elseTokens, interpreter, locals)
+ elif self.type == 'for':
+ sides = self.IN_RE.split(self.rest, 1)
+ if len(sides) != 2:
+ raise ParseError, "control expected 'for x in seq'"
+ iterator, sequenceCode = sides
+ info = self.build(['else'])
+ elseTokens = None
+ if info[-1][0].type == 'else':
+ elseTokens = info.pop()[1]
+ if len(info) != 1:
+ raise ParseError, "control 'for' expects at most one 'else'"
+ sequence = interpreter.evaluate(sequenceCode, locals)
+ for element in sequence:
+ try:
+ interpreter.assign(iterator, element, locals)
+ self.subrun(info[0][1], interpreter, locals)
+ except ContinueFlow:
+ continue
+ except BreakFlow:
+ break
+ else:
+ if elseTokens:
+ self.subrun(elseTokens, interpreter, locals)
+ elif self.type == 'while':
+ testCode = self.rest
+ info = self.build(['else'])
+ elseTokens = None
+ if info[-1][0].type == 'else':
+ elseTokens = info.pop()[1]
+ if len(info) != 1:
+ raise ParseError, "control 'while' expects at most one 'else'"
+ atLeastOnce = False
+ while True:
+ try:
+ if not interpreter.evaluate(testCode, locals):
+ break
+ atLeastOnce = True
+ self.subrun(info[0][1], interpreter, locals)
+ except ContinueFlow:
+ continue
+ except BreakFlow:
+ break
+ if not atLeastOnce and elseTokens:
+ self.subrun(elseTokens, interpreter, locals)
+ elif self.type == 'try':
+ info = self.build(['except', 'finally'])
+ if len(info) == 1:
+ raise ParseError, "control 'try' needs 'except' or 'finally'"
+ type = info[-1][0].type
+ if type == 'except':
+ for secondary, _tokens in info[1:]:
+ if secondary.type != 'except':
+ raise ParseError, \
+ "control 'try' cannot have 'except' and 'finally'"
+ else:
+ assert type == 'finally'
+ if len(info) != 2:
+ raise ParseError, \
+ "control 'try' can only have one 'finally'"
+ if type == 'except':
+ try:
+ self.subrun(info[0][1], interpreter, locals)
+ except FlowError:
+ raise
+ except Exception, e:
+ for secondary, tokens in info[1:]:
+ exception, variable = interpreter.clause(secondary.rest)
+ if variable is not None:
+ interpreter.assign(variable, e)
+ if isinstance(e, exception):
+ self.subrun(tokens, interpreter, locals)
+ break
+ else:
+ raise
+ else:
+ try:
+ self.subrun(info[0][1], interpreter, locals)
+ finally:
+ self.subrun(info[1][1], interpreter, locals)
+ elif self.type == 'continue':
+ raise ContinueFlow, "control 'continue' without 'for', 'while'"
+ elif self.type == 'break':
+ raise BreakFlow, "control 'break' without 'for', 'while'"
+ elif self.type == 'def':
+ signature = self.rest
+ definition = self.substring()
+ code = 'def %s:\n' \
+ ' r"""%s"""\n' \
+ ' return %s.expand(r"""%s""", locals())\n' % \
+ (signature, definition, interpreter.pseudo, definition)
+ interpreter.execute(code, locals)
+ elif self.type == 'end':
+ raise ParseError, "control 'end' requires primary markup"
+ else:
+ raise ParseError, \
+ "control '%s' cannot be at this level" % self.type
+ interpreter.invoke('afterControl')
+
+ def subrun(self, tokens, interpreter, locals):
+ """Execute a sequence of tokens."""
+ for token in tokens:
+ token.run(interpreter, locals)
+
+ def substring(self):
+ return string.join(map(str, self.subtokens), '')
+
+ def string(self):
+ if self.kind == 'primary':
+ return '%s[%s]%s%s[end %s]' % \
+ (self.prefix, self.contents, self.substring(), \
+ self.prefix, self.type)
+ else:
+ return '%s[%s]' % (self.prefix, self.contents)
+
+
+class Scanner:
+
+ """A scanner holds a buffer for lookahead parsing and has the
+ ability to scan for special symbols and indicators in that
+ buffer."""
+
+ # This is the token mapping table that maps first characters to
+ # token classes.
+ TOKEN_MAP = [
+ (None, PrefixToken),
+ (' \t\v\r\n', WhitespaceToken),
+ (')]}', LiteralToken),
+ ('\\', EscapeToken),
+ ('#', CommentToken),
+ ('?', ContextNameToken),
+ ('!', ContextLineToken),
+ ('%', SignificatorToken),
+ ('(', ExpressionToken),
+ (IDENTIFIER_FIRST_CHARS, SimpleExpressionToken),
+ ('\'\"', StringLiteralToken),
+ ('`', ReprToken),
+ (':', InPlaceToken),
+ ('[', ControlToken),
+ ('{', StatementToken),
+ ('<', CustomToken),
+ ]
+
+ def __init__(self, prefix, data=''):
+ self.prefix = prefix
+ self.pointer = 0
+ self.buffer = data
+ self.lock = 0
+
+ def __nonzero__(self): return self.pointer < len(self.buffer)
+ def __len__(self): return len(self.buffer) - self.pointer
+ def __getitem__(self, index): return self.buffer[self.pointer + index]
+
+ def __getslice__(self, start, stop):
+ if stop > len(self):
+ stop = len(self)
+ return self.buffer[self.pointer + start:self.pointer + stop]
+
+ def advance(self, count=1):
+ """Advance the pointer count characters."""
+ self.pointer = self.pointer + count
+
+ def retreat(self, count=1):
+ self.pointer = self.pointer - count
+ if self.pointer < 0:
+ raise ParseError, "can't retreat back over synced out chars"
+
+ def set(self, data):
+ """Start the scanner digesting a new batch of data; start the pointer
+ over from scratch."""
+ self.pointer = 0
+ self.buffer = data
+
+ def feed(self, data):
+ """Feed some more data to the scanner."""
+ self.buffer = self.buffer + data
+
+ def chop(self, count=None, slop=0):
+ """Chop the first count + slop characters off the front, and return
+ the first count. If count is not specified, then return
+ everything."""
+ if count is None:
+ assert slop == 0
+ count = len(self)
+ if count > len(self):
+ raise TransientParseError, "not enough data to read"
+ result = self[:count]
+ self.advance(count + slop)
+ return result
+
+ def acquire(self):
+ """Lock the scanner so it doesn't destroy data on sync."""
+ self.lock = self.lock + 1
+
+ def release(self):
+ """Unlock the scanner."""
+ self.lock = self.lock - 1
+
+ def sync(self):
+ """Sync up the buffer with the read head."""
+ if self.lock == 0 and self.pointer != 0:
+ self.buffer = self.buffer[self.pointer:]
+ self.pointer = 0
+
+ def unsync(self):
+ """Undo changes; reset the read head."""
+ if self.pointer != 0:
+ self.lock = 0
+ self.pointer = 0
+
+ def rest(self):
+ """Get the remainder of the buffer."""
+ return self[:]
+
+ def read(self, i=0, count=1):
+ """Read count chars starting from i; raise a transient error if
+ there aren't enough characters remaining."""
+ if len(self) < i + count:
+ raise TransientParseError, "need more data to read"
+ else:
+ return self[i:i + count]
+
+ def check(self, i, archetype=None):
+ """Scan for the next single or triple quote, with the specified
+ archetype. Return the found quote or None."""
+ quote = None
+ if self[i] in '\'\"':
+ quote = self[i]
+ if len(self) - i < 3:
+ for j in range(i, len(self)):
+ if self[i] == quote:
+ return quote
+ else:
+ raise TransientParseError, "need to scan for rest of quote"
+ if self[i + 1] == self[i + 2] == quote:
+ quote = quote * 3
+ if quote is not None:
+ if archetype is None:
+ return quote
+ else:
+ if archetype == quote:
+ return quote
+ elif len(archetype) < len(quote) and archetype[0] == quote[0]:
+ return archetype
+ else:
+ return None
+ else:
+ return None
+
+ def find(self, sub, start=0, end=None):
+ """Find the next occurrence of the character, or return -1."""
+ if end is not None:
+ return string.find(self.rest(), sub, start, end)
+ else:
+ return string.find(self.rest(), sub, start)
+
+ def last(self, char, start=0, end=None):
+ """Find the first character that is _not_ the specified character."""
+ if end is None:
+ end = len(self)
+ i = start
+ while i < end:
+ if self[i] != char:
+ return i
+ i = i + 1
+ else:
+ raise TransientParseError, "expecting other than %s" % char
+
+ def next(self, target, start=0, end=None, mandatory=False):
+ """Scan for the next occurrence of one of the characters in
+ the target string; optionally, make the scan mandatory."""
+ if mandatory:
+ assert end is not None
+ quote = None
+ if end is None:
+ end = len(self)
+ i = start
+ while i < end:
+ newQuote = self.check(i, quote)
+ if newQuote:
+ if newQuote == quote:
+ quote = None
+ else:
+ quote = newQuote
+ i = i + len(newQuote)
+ else:
+ c = self[i]
+ if quote:
+ if c == '\\':
+ i = i + 1
+ else:
+ if c in target:
+ return i
+ i = i + 1
+ else:
+ if mandatory:
+ raise ParseError, "expecting %s, not found" % target
+ else:
+ raise TransientParseError, "expecting ending character"
+
+ def quote(self, start=0, end=None, mandatory=False):
+ """Scan for the end of the next quote."""
+ assert self[start] in '\'\"'
+ quote = self.check(start)
+ if end is None:
+ end = len(self)
+ i = start + len(quote)
+ while i < end:
+ newQuote = self.check(i, quote)
+ if newQuote:
+ i = i + len(newQuote)
+ if newQuote == quote:
+ return i
+ else:
+ c = self[i]
+ if c == '\\':
+ i = i + 1
+ i = i + 1
+ else:
+ if mandatory:
+ raise ParseError, "expecting end of string literal"
+ else:
+ raise TransientParseError, "expecting end of string literal"
+
+ def nested(self, enter, exit, start=0, end=None):
+ """Scan from i for an ending sequence, respecting entries and exits
+ only."""
+ depth = 0
+ if end is None:
+ end = len(self)
+ i = start
+ while i < end:
+ c = self[i]
+ if c == enter:
+ depth = depth + 1
+ elif c == exit:
+ depth = depth - 1
+ if depth < 0:
+ return i
+ i = i + 1
+ else:
+ raise TransientParseError, "expecting end of complex expression"
+
+ def complex(self, enter, exit, start=0, end=None, skip=None):
+ """Scan from i for an ending sequence, respecting quotes,
+ entries and exits."""
+ quote = None
+ depth = 0
+ if end is None:
+ end = len(self)
+ last = None
+ i = start
+ while i < end:
+ newQuote = self.check(i, quote)
+ if newQuote:
+ if newQuote == quote:
+ quote = None
+ else:
+ quote = newQuote
+ i = i + len(newQuote)
+ else:
+ c = self[i]
+ if quote:
+ if c == '\\':
+ i = i + 1
+ else:
+ if skip is None or last != skip:
+ if c == enter:
+ depth = depth + 1
+ elif c == exit:
+ depth = depth - 1
+ if depth < 0:
+ return i
+ last = c
+ i = i + 1
+ else:
+ raise TransientParseError, "expecting end of complex expression"
+
+ def word(self, start=0):
+ """Scan from i for a simple word."""
+ length = len(self)
+ i = start
+ while i < length:
+ if not self[i] in IDENTIFIER_CHARS:
+ return i
+ i = i + 1
+ else:
+ raise TransientParseError, "expecting end of word"
+
+ def phrase(self, start=0):
+ """Scan from i for a phrase (e.g., 'word', 'f(a, b, c)', 'a[i]', or
+ combinations like 'x[i](a)'."""
+ # Find the word.
+ i = self.word(start)
+ while i < len(self) and self[i] in '([{':
+ enter = self[i]
+ if enter == '{':
+ raise ParseError, "curly braces can't open simple expressions"
+ exit = ENDING_CHARS[enter]
+ i = self.complex(enter, exit, i + 1) + 1
+ return i
+
+ def simple(self, start=0):
+ """Scan from i for a simple expression, which consists of one
+ more phrases separated by dots."""
+ i = self.phrase(start)
+ length = len(self)
+ while i < length and self[i] == '.':
+ i = self.phrase(i)
+ # Make sure we don't end with a trailing dot.
+ while i > 0 and self[i - 1] == '.':
+ i = i - 1
+ return i
+
+ def one(self):
+ """Parse and return one token, or None if the scanner is empty."""
+ if not self:
+ return None
+ if not self.prefix:
+ loc = -1
+ else:
+ loc = self.find(self.prefix)
+ if loc < 0:
+ # If there's no prefix in the buffer, then set the location to
+ # the end so the whole thing gets processed.
+ loc = len(self)
+ if loc == 0:
+ # If there's a prefix at the beginning of the buffer, process
+ # an expansion.
+ prefix = self.chop(1)
+ assert prefix == self.prefix
+ first = self.chop(1)
+ if first == self.prefix:
+ first = None
+ for firsts, factory in self.TOKEN_MAP:
+ if firsts is None:
+ if first is None:
+ break
+ elif first in firsts:
+ break
+ else:
+ raise ParseError, "unknown markup: %s%s" % (self.prefix, first)
+ token = factory(self.prefix, first)
+ try:
+ token.scan(self)
+ except TransientParseError:
+ # If a transient parse error occurs, reset the buffer pointer
+ # so we can (conceivably) try again later.
+ self.unsync()
+ raise
+ else:
+ # Process everything up to loc as a null token.
+ data = self.chop(loc)
+ token = NullToken(data)
+ self.sync()
+ return token
+
+
+class Interpreter:
+
+ """An interpreter can process chunks of EmPy code."""
+
+ # Constants.
+
+ VERSION = __version__
+ SIGNIFICATOR_RE_SUFFIX = SIGNIFICATOR_RE_SUFFIX
+ SIGNIFICATOR_RE_STRING = None
+
+ # Types.
+
+ Interpreter = None # define this below to prevent a circular reference
+ Hook = Hook # DEPRECATED
+ Filter = Filter # DEPRECATED
+ NullFilter = NullFilter # DEPRECATED
+ FunctionFilter = FunctionFilter # DEPRECATED
+ StringFilter = StringFilter # DEPRECATED
+ BufferedFilter = BufferedFilter # DEPRECATED
+ SizeBufferedFilter = SizeBufferedFilter # DEPRECATED
+ LineBufferedFilter = LineBufferedFilter # DEPRECATED
+ MaximallyBufferedFilter = MaximallyBufferedFilter # DEPRECATED
+
+ # Tables.
+
+ ESCAPE_CODES = {0x00: '0', 0x07: 'a', 0x08: 'b', 0x1b: 'e', 0x0c: 'f', \
+ 0x7f: 'h', 0x0a: 'n', 0x0d: 'r', 0x09: 't', 0x0b: 'v', \
+ 0x04: 'z'}
+
+ ASSIGN_TOKEN_RE = re.compile(r"[_a-zA-Z][_a-zA-Z0-9]*|\(|\)|,")
+
+ DEFAULT_OPTIONS = {BANGPATH_OPT: True,
+ BUFFERED_OPT: False,
+ RAW_OPT: False,
+ EXIT_OPT: True,
+ FLATTEN_OPT: False,
+ OVERRIDE_OPT: True,
+ CALLBACK_OPT: False}
+
+ _wasProxyInstalled = False # was a proxy installed?
+
+ # Construction, initialization, destruction.
+
+ def __init__(self, output=None, argv=None, prefix=DEFAULT_PREFIX, \
+ pseudo=None, options=None, globals=None, hooks=None):
+ self.interpreter = self # DEPRECATED
+ # Set up the stream.
+ if output is None:
+ output = UncloseableFile(sys.__stdout__)
+ self.output = output
+ self.prefix = prefix
+ if pseudo is None:
+ pseudo = DEFAULT_PSEUDOMODULE_NAME
+ self.pseudo = pseudo
+ if argv is None:
+ argv = [DEFAULT_SCRIPT_NAME]
+ self.argv = argv
+ self.args = argv[1:]
+ if options is None:
+ options = {}
+ self.options = options
+ # Initialize any hooks.
+ self.hooksEnabled = None # special sentinel meaning "false until added"
+ self.hooks = []
+ if hooks is None:
+ hooks = []
+ for hook in hooks:
+ self.register(hook)
+ # Initialize callback.
+ self.callback = None
+ # Finalizers.
+ self.finals = []
+ # The interpreter stacks.
+ self.contexts = Stack()
+ self.streams = Stack()
+ # Now set up the globals.
+ self.globals = globals
+ self.fix()
+ self.history = Stack()
+ # Install a proxy stdout if one hasn't been already.
+ self.installProxy()
+ # Finally, reset the state of all the stacks.
+ self.reset()
+ # Okay, now flatten the namespaces if that option has been set.
+ if self.options.get(FLATTEN_OPT, False):
+ self.flatten()
+ # Set up old pseudomodule attributes.
+ if prefix is None:
+ self.SIGNIFICATOR_RE_STRING = None
+ else:
+ self.SIGNIFICATOR_RE_STRING = prefix + self.SIGNIFICATOR_RE_SUFFIX
+ self.Interpreter = self.__class__
+ # Done. Now declare that we've started up.
+ self.invoke('atStartup')
+
+ def __del__(self):
+ self.shutdown()
+
+ def __repr__(self):
+ return '<%s pseudomodule/interpreter at 0x%x>' % \
+ (self.pseudo, id(self))
+
+ def ready(self):
+ """Declare the interpreter ready for normal operations."""
+ self.invoke('atReady')
+
+ def fix(self):
+ """Reset the globals, stamping in the pseudomodule."""
+ if self.globals is None:
+ self.globals = {}
+ # Make sure that there is no collision between two interpreters'
+ # globals.
+ if self.globals.has_key(self.pseudo):
+ if self.globals[self.pseudo] is not self:
+ raise Error, "interpreter globals collision"
+ self.globals[self.pseudo] = self
+
+ def unfix(self):
+ """Remove the pseudomodule (if present) from the globals."""
+ UNWANTED_KEYS = [self.pseudo, '__builtins__']
+ for unwantedKey in UNWANTED_KEYS:
+ if self.globals.has_key(unwantedKey):
+ del self.globals[unwantedKey]
+
+ def update(self, other):
+ """Update the current globals dictionary with another dictionary."""
+ self.globals.update(other)
+ self.fix()
+
+ def clear(self):
+ """Clear out the globals dictionary with a brand new one."""
+ self.globals = {}
+ self.fix()
+
+ def save(self, deep=True):
+ if deep:
+ copyMethod = copy.deepcopy
+ else:
+ copyMethod = copy.copy
+ """Save a copy of the current globals on the history stack."""
+ self.unfix()
+ self.history.push(copyMethod(self.globals))
+ self.fix()
+
+ def restore(self, destructive=True):
+ """Restore the topmost historic globals."""
+ if destructive:
+ fetchMethod = self.history.pop
+ else:
+ fetchMethod = self.history.top
+ self.unfix()
+ self.globals = fetchMethod()
+ self.fix()
+
+ def shutdown(self):
+ """Declare this interpreting session over; close the stream file
+ object. This method is idempotent."""
+ if self.streams is not None:
+ try:
+ self.finalize()
+ self.invoke('atShutdown')
+ while self.streams:
+ stream = self.streams.pop()
+ stream.close()
+ finally:
+ self.streams = None
+
+ def ok(self):
+ """Is the interpreter still active?"""
+ return self.streams is not None
+
+ # Writeable file-like methods.
+
+ def write(self, data):
+ self.stream().write(data)
+
+ def writelines(self, stuff):
+ self.stream().writelines(stuff)
+
+ def flush(self):
+ self.stream().flush()
+
+ def close(self):
+ self.shutdown()
+
+ # Stack-related activity.
+
+ def context(self):
+ return self.contexts.top()
+
+ def stream(self):
+ return self.streams.top()
+
+ def reset(self):
+ self.contexts.purge()
+ self.streams.purge()
+ self.streams.push(Stream(self.output))
+ if self.options.get(OVERRIDE_OPT, True):
+ sys.stdout.clear(self)
+
+ def push(self):
+ if self.options.get(OVERRIDE_OPT, True):
+ sys.stdout.push(self)
+
+ def pop(self):
+ if self.options.get(OVERRIDE_OPT, True):
+ sys.stdout.pop(self)
+
+ # Higher-level operations.
+
+ def include(self, fileOrFilename, locals=None):
+ """Do an include pass on a file or filename."""
+ if type(fileOrFilename) is types.StringType:
+ # Either it's a string representing a filename ...
+ filename = fileOrFilename
+ name = filename
+ file = theSubsystem.open(filename, 'r')
+ else:
+ # ... or a file object.
+ file = fileOrFilename
+ name = "<%s>" % str(file.__class__)
+ self.invoke('beforeInclude', name=name, file=file, locals=locals)
+ self.file(file, name, locals)
+ self.invoke('afterInclude')
+
+ def expand(self, data, locals=None):
+ """Do an explicit expansion on a subordinate stream."""
+ outFile = StringIO.StringIO()
+ stream = Stream(outFile)
+ self.invoke('beforeExpand', string=data, locals=locals)
+ self.streams.push(stream)
+ try:
+ self.string(data, '<expand>', locals)
+ stream.flush()
+ expansion = outFile.getvalue()
+ self.invoke('afterExpand', result=expansion)
+ return expansion
+ finally:
+ self.streams.pop()
+
+ def quote(self, data):
+ """Quote the given string so that if it were expanded it would
+ evaluate to the original."""
+ self.invoke('beforeQuote', string=data)
+ scanner = Scanner(self.prefix, data)
+ result = []
+ i = 0
+ try:
+ j = scanner.next(self.prefix, i)
+ result.append(data[i:j])
+ result.append(self.prefix * 2)
+ i = j + 1
+ except TransientParseError:
+ pass
+ result.append(data[i:])
+ result = string.join(result, '')
+ self.invoke('afterQuote', result=result)
+ return result
+
+ def escape(self, data, more=''):
+ """Escape a string so that nonprintable characters are replaced
+ with compatible EmPy expansions."""
+ self.invoke('beforeEscape', string=data, more=more)
+ result = []
+ for char in data:
+ if char < ' ' or char > '~':
+ charOrd = ord(char)
+ if Interpreter.ESCAPE_CODES.has_key(charOrd):
+ result.append(self.prefix + '\\' + \
+ Interpreter.ESCAPE_CODES[charOrd])
+ else:
+ result.append(self.prefix + '\\x%02x' % charOrd)
+ elif char in more:
+ result.append(self.prefix + '\\' + char)
+ else:
+ result.append(char)
+ result = string.join(result, '')
+ self.invoke('afterEscape', result=result)
+ return result
+
+ # Processing.
+
+ def wrap(self, callable, args):
+ """Wrap around an application of a callable and handle errors.
+ Return whether no error occurred."""
+ try:
+ apply(callable, args)
+ self.reset()
+ return True
+ except KeyboardInterrupt, e:
+ # Handle keyboard interrupts specially: we should always exit
+ # from these.
+ self.fail(e, True)
+ except Exception, e:
+ # A standard exception (other than a keyboard interrupt).
+ self.fail(e)
+ except:
+ # If we get here, then either it's an exception not derived from
+ # Exception or it's a string exception, so get the error type
+ # from the sys module.
+ e = sys.exc_type
+ self.fail(e)
+ # An error occurred if we leak through to here, so do cleanup.
+ self.reset()
+ return False
+
+ def interact(self):
+ """Perform interaction."""
+ self.invoke('atInteract')
+ done = False
+ while not done:
+ result = self.wrap(self.file, (sys.stdin, '<interact>'))
+ if self.options.get(EXIT_OPT, True):
+ done = True
+ else:
+ if result:
+ done = True
+ else:
+ self.reset()
+
+ def fail(self, error, fatal=False):
+ """Handle an actual error that occurred."""
+ if self.options.get(BUFFERED_OPT, False):
+ try:
+ self.output.abort()
+ except AttributeError:
+ # If the output file object doesn't have an abort method,
+ # something got mismatched, but it's too late to do
+ # anything about it now anyway, so just ignore it.
+ pass
+ meta = self.meta(error)
+ self.handle(meta)
+ if self.options.get(RAW_OPT, False):
+ raise
+ if fatal or self.options.get(EXIT_OPT, True):
+ sys.exit(FAILURE_CODE)
+
+ def file(self, file, name='<file>', locals=None):
+ """Parse the entire contents of a file-like object, line by line."""
+ context = Context(name)
+ self.contexts.push(context)
+ self.invoke('beforeFile', name=name, file=file, locals=locals)
+ scanner = Scanner(self.prefix)
+ first = True
+ done = False
+ while not done:
+ self.context().bump()
+ line = file.readline()
+ if first:
+ if self.options.get(BANGPATH_OPT, True) and self.prefix:
+ # Replace a bangpath at the beginning of the first line
+ # with an EmPy comment.
+ if string.find(line, BANGPATH) == 0:
+ line = self.prefix + '#' + line[2:]
+ first = False
+ if line:
+ scanner.feed(line)
+ else:
+ done = True
+ self.safe(scanner, done, locals)
+ self.invoke('afterFile')
+ self.contexts.pop()
+
+ def binary(self, file, name='<binary>', chunkSize=0, locals=None):
+ """Parse the entire contents of a file-like object, in chunks."""
+ if chunkSize <= 0:
+ chunkSize = DEFAULT_CHUNK_SIZE
+ context = Context(name, units='bytes')
+ self.contexts.push(context)
+ self.invoke('beforeBinary', name=name, file=file, \
+ chunkSize=chunkSize, locals=locals)
+ scanner = Scanner(self.prefix)
+ done = False
+ while not done:
+ chunk = file.read(chunkSize)
+ if chunk:
+ scanner.feed(chunk)
+ else:
+ done = True
+ self.safe(scanner, done, locals)
+ self.context().bump(len(chunk))
+ self.invoke('afterBinary')
+ self.contexts.pop()
+
+ def string(self, data, name='<string>', locals=None):
+ """Parse a string."""
+ context = Context(name)
+ self.contexts.push(context)
+ self.invoke('beforeString', name=name, string=data, locals=locals)
+ context.bump()
+ scanner = Scanner(self.prefix, data)
+ self.safe(scanner, True, locals)
+ self.invoke('afterString')
+ self.contexts.pop()
+
+ def safe(self, scanner, final=False, locals=None):
+ """Do a protected parse. Catch transient parse errors; if
+ final is true, then make a final pass with a terminator,
+ otherwise ignore the transient parse error (more data is
+ pending)."""
+ try:
+ self.parse(scanner, locals)
+ except TransientParseError:
+ if final:
+ # If the buffer doesn't end with a newline, try tacking on
+ # a dummy terminator.
+ buffer = scanner.rest()
+ if buffer and buffer[-1] != '\n':
+ scanner.feed(self.prefix + '\n')
+ # A TransientParseError thrown from here is a real parse
+ # error.
+ self.parse(scanner, locals)
+
+ def parse(self, scanner, locals=None):
+ """Parse and run as much from this scanner as possible."""
+ self.invoke('atParse', scanner=scanner, locals=locals)
+ while True:
+ token = scanner.one()
+ if token is None:
+ break
+ self.invoke('atToken', token=token)
+ token.run(self, locals)
+
+ # Medium-level evaluation and execution.
+
+ def tokenize(self, name):
+ """Take an lvalue string and return a name or a (possibly recursive)
+ list of names."""
+ result = []
+ stack = [result]
+ for garbage in self.ASSIGN_TOKEN_RE.split(name):
+ garbage = string.strip(garbage)
+ if garbage:
+ raise ParseError, "unexpected assignment token: '%s'" % garbage
+ tokens = self.ASSIGN_TOKEN_RE.findall(name)
+ # While processing, put a None token at the start of any list in which
+ # commas actually appear.
+ for token in tokens:
+ if token == '(':
+ stack.append([])
+ elif token == ')':
+ top = stack.pop()
+ if len(top) == 1:
+ top = top[0] # no None token means that it's not a 1-tuple
+ elif top[0] is None:
+ del top[0] # remove the None token for real tuples
+ stack[-1].append(top)
+ elif token == ',':
+ if len(stack[-1]) == 1:
+ stack[-1].insert(0, None)
+ else:
+ stack[-1].append(token)
+ # If it's a 1-tuple at the top level, turn it into a real subsequence.
+ if result and result[0] is None:
+ result = [result[1:]]
+ if len(result) == 1:
+ return result[0]
+ else:
+ return result
+
+ def significate(self, key, value=None, locals=None):
+ """Declare a significator."""
+ self.invoke('beforeSignificate', key=key, value=value, locals=locals)
+ name = '__%s__' % key
+ self.atomic(name, value, locals)
+ self.invoke('afterSignificate')
+
+ def atomic(self, name, value, locals=None):
+ """Do an atomic assignment."""
+ self.invoke('beforeAtomic', name=name, value=value, locals=locals)
+ if locals is None:
+ self.globals[name] = value
+ else:
+ locals[name] = value
+ self.invoke('afterAtomic')
+
+ def multi(self, names, values, locals=None):
+ """Do a (potentially recursive) assignment."""
+ self.invoke('beforeMulti', names=names, values=values, locals=locals)
+ # No zip in 1.5, so we have to do it manually.
+ i = 0
+ try:
+ values = tuple(values)
+ except TypeError:
+ raise TypeError, "unpack non-sequence"
+ if len(names) != len(values):
+ raise ValueError, "unpack tuple of wrong size"
+ for i in range(len(names)):
+ name = names[i]
+ if type(name) is types.StringType:
+ self.atomic(name, values[i], locals)
+ else:
+ self.multi(name, values[i], locals)
+ self.invoke('afterMulti')
+
+ def assign(self, name, value, locals=None):
+ """Do a potentially complex (including tuple unpacking) assignment."""
+ left = self.tokenize(name)
+ # The return value of tokenize can either be a string or a list of
+ # (lists of) strings.
+ if type(left) is types.StringType:
+ self.atomic(left, value, locals)
+ else:
+ self.multi(left, value, locals)
+
+ def import_(self, name, locals=None):
+ """Do an import."""
+ self.invoke('beforeImport', name=name, locals=locals)
+ self.execute('import %s' % name, locals)
+ self.invoke('afterImport')
+
+ def clause(self, catch, locals=None):
+ """Given the string representation of an except clause, turn it into
+ a 2-tuple consisting of the class name, and either a variable name
+ or None."""
+ self.invoke('beforeClause', catch=catch, locals=locals)
+ if catch is None:
+ exceptionCode, variable = None, None
+ elif string.find(catch, ',') >= 0:
+ exceptionCode, variable = string.split(string.strip(catch), ',', 1)
+ variable = string.strip(variable)
+ else:
+ exceptionCode, variable = string.strip(catch), None
+ if not exceptionCode:
+ exception = Exception
+ else:
+ exception = self.evaluate(exceptionCode, locals)
+ self.invoke('afterClause', exception=exception, variable=variable)
+ return exception, variable
+
+ def serialize(self, expression, locals=None):
+ """Do an expansion, involving evaluating an expression, then
+ converting it to a string and writing that string to the
+ output if the evaluation is not None."""
+ self.invoke('beforeSerialize', expression=expression, locals=locals)
+ result = self.evaluate(expression, locals)
+ if result is not None:
+ self.write(str(result))
+ self.invoke('afterSerialize')
+
+ def defined(self, name, locals=None):
+ """Return a Boolean indicating whether or not the name is
+ defined either in the locals or the globals."""
+ self.invoke('beforeDefined', name=name, local=local)
+ if locals is not None:
+ if locals.has_key(name):
+ result = True
+ else:
+ result = False
+ elif self.globals.has_key(name):
+ result = True
+ else:
+ result = False
+ self.invoke('afterDefined', result=result)
+
+ def literal(self, text):
+ """Process a string literal."""
+ self.invoke('beforeLiteral', text=text)
+ self.serialize(text)
+ self.invoke('afterLiteral')
+
+ # Low-level evaluation and execution.
+
+ def evaluate(self, expression, locals=None):
+ """Evaluate an expression."""
+ if expression in ('1', 'True'): return True
+ if expression in ('0', 'False'): return False
+ self.push()
+ try:
+ self.invoke('beforeEvaluate', \
+ expression=expression, locals=locals)
+ if locals is not None:
+ result = eval(expression, self.globals, locals)
+ else:
+ result = eval(expression, self.globals)
+ self.invoke('afterEvaluate', result=result)
+ return result
+ finally:
+ self.pop()
+
+ def execute(self, statements, locals=None):
+ """Execute a statement."""
+ # If there are any carriage returns (as opposed to linefeeds/newlines)
+ # in the statements code, then remove them. Even on DOS/Windows
+ # platforms,
+ if string.find(statements, '\r') >= 0:
+ statements = string.replace(statements, '\r', '')
+ # If there are no newlines in the statements code, then strip any
+ # leading or trailing whitespace.
+ if string.find(statements, '\n') < 0:
+ statements = string.strip(statements)
+ self.push()
+ try:
+ self.invoke('beforeExecute', \
+ statements=statements, locals=locals)
+ if locals is not None:
+ exec statements in self.globals, locals
+ else:
+ exec statements in self.globals
+ self.invoke('afterExecute')
+ finally:
+ self.pop()
+
+ def single(self, source, locals=None):
+ """Execute an expression or statement, just as if it were
+ entered into the Python interactive interpreter."""
+ self.push()
+ try:
+ self.invoke('beforeSingle', \
+ source=source, locals=locals)
+ code = compile(source, '<single>', 'single')
+ if locals is not None:
+ exec code in self.globals, locals
+ else:
+ exec code in self.globals
+ self.invoke('afterSingle')
+ finally:
+ self.pop()
+
+ # Hooks.
+
+ def register(self, hook, prepend=False):
+ """Register the provided hook."""
+ hook.register(self)
+ if self.hooksEnabled is None:
+ # A special optimization so that hooks can be effectively
+ # disabled until one is added or they are explicitly turned on.
+ self.hooksEnabled = True
+ if prepend:
+ self.hooks.insert(0, hook)
+ else:
+ self.hooks.append(hook)
+
+ def deregister(self, hook):
+ """Remove an already registered hook."""
+ hook.deregister(self)
+ self.hooks.remove(hook)
+
+ def invoke(self, _name, **keywords):
+ """Invoke the hook(s) associated with the hook name, should they
+ exist."""
+ if self.hooksEnabled:
+ for hook in self.hooks:
+ hook.push()
+ try:
+ method = getattr(hook, _name)
+ apply(method, (), keywords)
+ finally:
+ hook.pop()
+
+ def finalize(self):
+ """Execute any remaining final routines."""
+ self.push()
+ self.invoke('atFinalize')
+ try:
+ # Pop them off one at a time so they get executed in reverse
+ # order and we remove them as they're executed in case something
+ # bad happens.
+ while self.finals:
+ final = self.finals.pop()
+ final()
+ finally:
+ self.pop()
+
+ # Error handling.
+
+ def meta(self, exc=None):
+ """Construct a MetaError for the interpreter's current state."""
+ return MetaError(self.contexts.clone(), exc)
+
+ def handle(self, meta):
+ """Handle a MetaError."""
+ first = True
+ self.invoke('atHandle', meta=meta)
+ for context in meta.contexts:
+ if first:
+ if meta.exc is not None:
+ desc = "error: %s: %s" % (meta.exc.__class__, meta.exc)
+ else:
+ desc = "error"
+ else:
+ desc = "from this context"
+ first = False
+ sys.stderr.write('%s: %s\n' % (context, desc))
+
+ def installProxy(self):
+ """Install a proxy if necessary."""
+ # Unfortunately, there's no surefire way to make sure that installing
+ # a sys.stdout proxy is idempotent, what with different interpreters
+ # running from different modules. The best we can do here is to try
+ # manipulating the proxy's test function ...
+ try:
+ sys.stdout._testProxy()
+ except AttributeError:
+ # ... if the current stdout object doesn't have one, then check
+ # to see if we think _this_ particularly Interpreter class has
+ # installed it before ...
+ if Interpreter._wasProxyInstalled:
+ # ... and if so, we have a proxy problem.
+ raise Error, "interpreter stdout proxy lost"
+ else:
+ # Otherwise, install the proxy and set the flag.
+ sys.stdout = ProxyFile(sys.stdout)
+ Interpreter._wasProxyInstalled = True
+
+ #
+ # Pseudomodule routines.
+ #
+
+ # Identification.
+
+ def identify(self):
+ """Identify the topmost context with a 2-tuple of the name and
+ line number."""
+ return self.context().identify()
+
+ def atExit(self, callable):
+ """Register a function to be called at exit."""
+ self.finals.append(callable)
+
+ # Context manipulation.
+
+ def pushContext(self, name='<unnamed>', line=0):
+ """Create a new context and push it."""
+ self.contexts.push(Context(name, line))
+
+ def popContext(self):
+ """Pop the top context."""
+ self.contexts.pop()
+
+ def setContextName(self, name):
+ """Set the name of the topmost context."""
+ context = self.context()
+ context.name = name
+
+ def setContextLine(self, line):
+ """Set the name of the topmost context."""
+ context = self.context()
+ context.line = line
+
+ setName = setContextName # DEPRECATED
+ setLine = setContextLine # DEPRECATED
+
+ # Globals manipulation.
+
+ def getGlobals(self):
+ """Retrieve the globals."""
+ return self.globals
+
+ def setGlobals(self, globals):
+ """Set the globals to the specified dictionary."""
+ self.globals = globals
+ self.fix()
+
+ def updateGlobals(self, otherGlobals):
+ """Merge another mapping object into this interpreter's globals."""
+ self.update(otherGlobals)
+
+ def clearGlobals(self):
+ """Clear out the globals with a brand new dictionary."""
+ self.clear()
+
+ def saveGlobals(self, deep=True):
+ """Save a copy of the globals off onto the history stack."""
+ self.save(deep)
+
+ def restoreGlobals(self, destructive=True):
+ """Restore the most recently saved copy of the globals."""
+ self.restore(destructive)
+
+ # Hook support.
+
+ def areHooksEnabled(self):
+ """Return whether or not hooks are presently enabled."""
+ if self.hooksEnabled is None:
+ return True
+ else:
+ return self.hooksEnabled
+
+ def enableHooks(self):
+ """Enable hooks."""
+ self.hooksEnabled = True
+
+ def disableHooks(self):
+ """Disable hooks."""
+ self.hooksEnabled = False
+
+ def getHooks(self):
+ """Get the current hooks."""
+ return self.hooks[:]
+
+ def clearHooks(self):
+ """Clear all hooks."""
+ self.hooks = []
+
+ def addHook(self, hook, prepend=False):
+ """Add a new hook; optionally insert it rather than appending it."""
+ self.register(hook, prepend)
+
+ def removeHook(self, hook):
+ """Remove a preexisting hook."""
+ self.deregister(hook)
+
+ def invokeHook(self, _name, **keywords):
+ """Manually invoke a hook."""
+ apply(self.invoke, (_name,), keywords)
+
+ # Callbacks.
+
+ def getCallback(self):
+ """Get the callback registered with this interpreter, or None."""
+ return self.callback
+
+ def registerCallback(self, callback):
+ """Register a custom markup callback with this interpreter."""
+ self.callback = callback
+
+ def deregisterCallback(self):
+ """Remove any previously registered callback with this interpreter."""
+ self.callback = None
+
+ def invokeCallback(self, contents):
+ """Invoke the callback."""
+ if self.callback is None:
+ if self.options.get(CALLBACK_OPT, False):
+ raise Error, "custom markup invoked with no defined callback"
+ else:
+ self.callback(contents)
+
+ # Pseudomodule manipulation.
+
+ def flatten(self, keys=None):
+ """Flatten the contents of the pseudo-module into the globals
+ namespace."""
+ if keys is None:
+ keys = self.__dict__.keys() + self.__class__.__dict__.keys()
+ dict = {}
+ for key in keys:
+ # The pseudomodule is really a class instance, so we need to
+ # fumble use getattr instead of simply fumbling through the
+ # instance's __dict__.
+ dict[key] = getattr(self, key)
+ # Stomp everything into the globals namespace.
+ self.globals.update(dict)
+
+ # Prefix.
+
+ def getPrefix(self):
+ """Get the current prefix."""
+ return self.prefix
+
+ def setPrefix(self, prefix):
+ """Set the prefix."""
+ self.prefix = prefix
+
+ # Diversions.
+
+ def stopDiverting(self):
+ """Stop any diverting."""
+ self.stream().revert()
+
+ def createDiversion(self, name):
+ """Create a diversion (but do not divert to it) if it does not
+ already exist."""
+ self.stream().create(name)
+
+ def retrieveDiversion(self, name):
+ """Retrieve the diversion object associated with the name."""
+ return self.stream().retrieve(name)
+
+ def startDiversion(self, name):
+ """Start diverting to the given diversion name."""
+ self.stream().divert(name)
+
+ def playDiversion(self, name):
+ """Play the given diversion and then purge it."""
+ self.stream().undivert(name, True)
+
+ def replayDiversion(self, name):
+ """Replay the diversion without purging it."""
+ self.stream().undivert(name, False)
+
+ def purgeDiversion(self, name):
+ """Eliminate the given diversion."""
+ self.stream().purge(name)
+
+ def playAllDiversions(self):
+ """Play all existing diversions and then purge them."""
+ self.stream().undivertAll(True)
+
+ def replayAllDiversions(self):
+ """Replay all existing diversions without purging them."""
+ self.stream().undivertAll(False)
+
+ def purgeAllDiversions(self):
+ """Purge all existing diversions."""
+ self.stream().purgeAll()
+
+ def getCurrentDiversion(self):
+ """Get the name of the current diversion."""
+ return self.stream().currentDiversion
+
+ def getAllDiversions(self):
+ """Get the names of all existing diversions."""
+ names = self.stream().diversions.keys()
+ names.sort()
+ return names
+
+ # Filter.
+
+ def resetFilter(self):
+ """Reset the filter so that it does no filtering."""
+ self.stream().install(None)
+
+ def nullFilter(self):
+ """Install a filter that will consume all text."""
+ self.stream().install(0)
+
+ def getFilter(self):
+ """Get the current filter."""
+ filter = self.stream().filter
+ if filter is self.stream().file:
+ return None
+ else:
+ return filter
+
+ def setFilter(self, shortcut):
+ """Set the filter."""
+ self.stream().install(shortcut)
+
+ def attachFilter(self, shortcut):
+ """Attach a single filter to the end of the current filter chain."""
+ self.stream().attach(shortcut)
+
+
+class Document:
+
+ """A representation of an individual EmPy document, as used by a
+ processor."""
+
+ def __init__(self, ID, filename):
+ self.ID = ID
+ self.filename = filename
+ self.significators = {}
+
+
+class Processor:
+
+ """An entity which is capable of processing a hierarchy of EmPy
+ files and building a dictionary of document objects associated
+ with them describing their significator contents."""
+
+ DEFAULT_EMPY_EXTENSIONS = ('.em',)
+ SIGNIFICATOR_RE = re.compile(SIGNIFICATOR_RE_STRING)
+
+ def __init__(self, factory=Document):
+ self.factory = factory
+ self.documents = {}
+
+ def identifier(self, pathname, filename): return filename
+
+ def clear(self):
+ self.documents = {}
+
+ def scan(self, basename, extensions=DEFAULT_EMPY_EXTENSIONS):
+ if type(extensions) is types.StringType:
+ extensions = (extensions,)
+ def _noCriteria(x):
+ return True
+ def _extensionsCriteria(pathname, extensions=extensions):
+ if extensions:
+ for extension in extensions:
+ if pathname[-len(extension):] == extension:
+ return True
+ return False
+ else:
+ return True
+ self.directory(basename, _noCriteria, _extensionsCriteria, None)
+ self.postprocess()
+
+ def postprocess(self):
+ pass
+
+ def directory(self, basename, dirCriteria, fileCriteria, depth=None):
+ if depth is not None:
+ if depth <= 0:
+ return
+ else:
+ depth = depth - 1
+ filenames = os.listdir(basename)
+ for filename in filenames:
+ pathname = os.path.join(basename, filename)
+ if os.path.isdir(pathname):
+ if dirCriteria(pathname):
+ self.directory(pathname, dirCriteria, fileCriteria, depth)
+ elif os.path.isfile(pathname):
+ if fileCriteria(pathname):
+ documentID = self.identifier(pathname, filename)
+ document = self.factory(documentID, pathname)
+ self.file(document, open(pathname))
+ self.documents[documentID] = document
+
+ def file(self, document, file):
+ while True:
+ line = file.readline()
+ if not line:
+ break
+ self.line(document, line)
+
+ def line(self, document, line):
+ match = self.SIGNIFICATOR_RE.search(line)
+ if match:
+ key, valueS = match.groups()
+ valueS = string.strip(valueS)
+ if valueS:
+ value = eval(valueS)
+ else:
+ value = None
+ document.significators[key] = value
+
+
+def expand(_data, _globals=None, \
+ _argv=None, _prefix=DEFAULT_PREFIX, _pseudo=None, _options=None, \
+ **_locals):
+ """Do an atomic expansion of the given source data, creating and
+ shutting down an interpreter dedicated to the task. The sys.stdout
+ object is saved off and then replaced before this function
+ returns."""
+ if len(_locals) == 0:
+ # If there were no keyword arguments specified, don't use a locals
+ # dictionary at all.
+ _locals = None
+ output = NullFile()
+ interpreter = Interpreter(output, argv=_argv, prefix=_prefix, \
+ pseudo=_pseudo, options=_options, \
+ globals=_globals)
+ if interpreter.options.get(OVERRIDE_OPT, True):
+ oldStdout = sys.stdout
+ try:
+ result = interpreter.expand(_data, _locals)
+ finally:
+ interpreter.shutdown()
+ if _globals is not None:
+ interpreter.unfix() # remove pseudomodule to prevent clashes
+ if interpreter.options.get(OVERRIDE_OPT, True):
+ sys.stdout = oldStdout
+ return result
+
+def environment(name, default=None):
+ """Get data from the current environment. If the default is True
+ or False, then presume that we're only interested in the existence
+ or non-existence of the environment variable."""
+ if os.environ.has_key(name):
+ # Do the True/False test by value for future compatibility.
+ if default == False or default == True:
+ return True
+ else:
+ return os.environ[name]
+ else:
+ return default
+
+def info(table):
+ DEFAULT_LEFT = 28
+ maxLeft = 0
+ maxRight = 0
+ for left, right in table:
+ if len(left) > maxLeft:
+ maxLeft = len(left)
+ if len(right) > maxRight:
+ maxRight = len(right)
+ FORMAT = ' %%-%ds %%s\n' % max(maxLeft, DEFAULT_LEFT)
+ for left, right in table:
+ if right.find('\n') >= 0:
+ for right in right.split('\n'):
+ sys.stderr.write(FORMAT % (left, right))
+ left = ''
+ else:
+ sys.stderr.write(FORMAT % (left, right))
+
+def usage(verbose=True):
+ """Print usage information."""
+ programName = sys.argv[0]
+ def warn(line=''):
+ sys.stderr.write("%s\n" % line)
+ warn("""\
+Usage: %s [options] [<filename, or '-' for stdin> [<argument>...]]
+Welcome to EmPy version %s.""" % (programName, __version__))
+ warn()
+ warn("Valid options:")
+ info(OPTION_INFO)
+ if verbose:
+ warn()
+ warn("The following markups are supported:")
+ info(MARKUP_INFO)
+ warn()
+ warn("Valid escape sequences are:")
+ info(ESCAPE_INFO)
+ warn()
+ warn("The %s pseudomodule contains the following attributes:" % \
+ DEFAULT_PSEUDOMODULE_NAME)
+ info(PSEUDOMODULE_INFO)
+ warn()
+ warn("The following environment variables are recognized:")
+ info(ENVIRONMENT_INFO)
+ warn()
+ warn(USAGE_NOTES)
+ else:
+ warn()
+ warn("Type %s -H for more extensive help." % programName)
+
+def invoke(args):
+ """Run a standalone instance of an EmPy interpeter."""
+ # Initialize the options.
+ _output = None
+ _options = {BUFFERED_OPT: environment(BUFFERED_ENV, False),
+ RAW_OPT: environment(RAW_ENV, False),
+ EXIT_OPT: True,
+ FLATTEN_OPT: environment(FLATTEN_ENV, False),
+ OVERRIDE_OPT: not environment(NO_OVERRIDE_ENV, False),
+ CALLBACK_OPT: False}
+ _preprocessing = []
+ _prefix = environment(PREFIX_ENV, DEFAULT_PREFIX)
+ _pseudo = environment(PSEUDO_ENV, None)
+ _interactive = environment(INTERACTIVE_ENV, False)
+ _extraArguments = environment(OPTIONS_ENV)
+ _binary = -1 # negative for not, 0 for default size, positive for size
+ _unicode = environment(UNICODE_ENV, False)
+ _unicodeInputEncoding = environment(INPUT_ENCODING_ENV, None)
+ _unicodeOutputEncoding = environment(OUTPUT_ENCODING_ENV, None)
+ _unicodeInputErrors = environment(INPUT_ERRORS_ENV, None)
+ _unicodeOutputErrors = environment(OUTPUT_ERRORS_ENV, None)
+ _hooks = []
+ _pauseAtEnd = False
+ _relativePath = False
+ if _extraArguments is not None:
+ _extraArguments = string.split(_extraArguments)
+ args = _extraArguments + args
+ # Parse the arguments.
+ pairs, remainder = getopt.getopt(args, 'VhHvkp:m:frino:a:buBP:I:D:E:F:', ['version', 'help', 'extended-help', 'verbose', 'null-hook', 'suppress-errors', 'prefix=', 'no-prefix', 'module=', 'flatten', 'raw-errors', 'interactive', 'no-override-stdout', 'binary', 'chunk-size=', 'output=' 'append=', 'preprocess=', 'import=', 'define=', 'execute=', 'execute-file=', 'buffered-output', 'pause-at-end', 'relative-path', 'no-callback-error', 'no-bangpath-processing', 'unicode', 'unicode-encoding=', 'unicode-input-encoding=', 'unicode-output-encoding=', 'unicode-errors=', 'unicode-input-errors=', 'unicode-output-errors='])
+ for option, argument in pairs:
+ if option in ('-V', '--version'):
+ sys.stderr.write("%s version %s\n" % (__program__, __version__))
+ return
+ elif option in ('-h', '--help'):
+ usage(False)
+ return
+ elif option in ('-H', '--extended-help'):
+ usage(True)
+ return
+ elif option in ('-v', '--verbose'):
+ _hooks.append(VerboseHook())
+ elif option in ('--null-hook',):
+ _hooks.append(Hook())
+ elif option in ('-k', '--suppress-errors'):
+ _options[EXIT_OPT] = False
+ _interactive = True # suppress errors implies interactive mode
+ elif option in ('-m', '--module'):
+ _pseudo = argument
+ elif option in ('-f', '--flatten'):
+ _options[FLATTEN_OPT] = True
+ elif option in ('-p', '--prefix'):
+ _prefix = argument
+ elif option in ('--no-prefix',):
+ _prefix = None
+ elif option in ('-r', '--raw-errors'):
+ _options[RAW_OPT] = True
+ elif option in ('-i', '--interactive'):
+ _interactive = True
+ elif option in ('-n', '--no-override-stdout'):
+ _options[OVERRIDE_OPT] = False
+ elif option in ('-o', '--output'):
+ _output = argument, 'w', _options[BUFFERED_OPT]
+ elif option in ('-a', '--append'):
+ _output = argument, 'a', _options[BUFFERED_OPT]
+ elif option in ('-b', '--buffered-output'):
+ _options[BUFFERED_OPT] = True
+ elif option in ('-B',): # DEPRECATED
+ _options[BUFFERED_OPT] = True
+ elif option in ('--binary',):
+ _binary = 0
+ elif option in ('--chunk-size',):
+ _binary = int(argument)
+ elif option in ('-P', '--preprocess'):
+ _preprocessing.append(('pre', argument))
+ elif option in ('-I', '--import'):
+ for module in string.split(argument, ','):
+ module = string.strip(module)
+ _preprocessing.append(('import', module))
+ elif option in ('-D', '--define'):
+ _preprocessing.append(('define', argument))
+ elif option in ('-E', '--execute'):
+ _preprocessing.append(('exec', argument))
+ elif option in ('-F', '--execute-file'):
+ _preprocessing.append(('file', argument))
+ elif option in ('-u', '--unicode'):
+ _unicode = True
+ elif option in ('--pause-at-end',):
+ _pauseAtEnd = True
+ elif option in ('--relative-path',):
+ _relativePath = True
+ elif option in ('--no-callback-error',):
+ _options[CALLBACK_OPT] = True
+ elif option in ('--no-bangpath-processing',):
+ _options[BANGPATH_OPT] = False
+ elif option in ('--unicode-encoding',):
+ _unicodeInputEncoding = _unicodeOutputEncoding = argument
+ elif option in ('--unicode-input-encoding',):
+ _unicodeInputEncoding = argument
+ elif option in ('--unicode-output-encoding',):
+ _unicodeOutputEncoding = argument
+ elif option in ('--unicode-errors',):
+ _unicodeInputErrors = _unicodeOutputErrors = argument
+ elif option in ('--unicode-input-errors',):
+ _unicodeInputErrors = argument
+ elif option in ('--unicode-output-errors',):
+ _unicodeOutputErrors = argument
+ # Set up the Unicode subsystem if required.
+ if _unicode or \
+ _unicodeInputEncoding or _unicodeOutputEncoding or \
+ _unicodeInputErrors or _unicodeOutputErrors:
+ theSubsystem.initialize(_unicodeInputEncoding, \
+ _unicodeOutputEncoding, \
+ _unicodeInputErrors, _unicodeOutputErrors)
+ # Now initialize the output file if something has already been selected.
+ if _output is not None:
+ _output = apply(AbstractFile, _output)
+ # Set up the main filename and the argument.
+ if not remainder:
+ remainder.append('-')
+ filename, arguments = remainder[0], remainder[1:]
+ # Set up the interpreter.
+ if _options[BUFFERED_OPT] and _output is None:
+ raise ValueError, "-b only makes sense with -o or -a arguments"
+ if _prefix == 'None':
+ _prefix = None
+ if _prefix and type(_prefix) is types.StringType and len(_prefix) != 1:
+ raise Error, "prefix must be single-character string"
+ interpreter = Interpreter(output=_output, \
+ argv=remainder, \
+ prefix=_prefix, \
+ pseudo=_pseudo, \
+ options=_options, \
+ hooks=_hooks)
+ try:
+ # Execute command-line statements.
+ i = 0
+ for which, thing in _preprocessing:
+ if which == 'pre':
+ command = interpreter.file
+ target = theSubsystem.open(thing, 'r')
+ name = thing
+ elif which == 'define':
+ command = interpreter.string
+ if string.find(thing, '=') >= 0:
+ target = '%s{%s}' % (_prefix, thing)
+ else:
+ target = '%s{%s = None}' % (_prefix, thing)
+ name = '<define:%d>' % i
+ elif which == 'exec':
+ command = interpreter.string
+ target = '%s{%s}' % (_prefix, thing)
+ name = '<exec:%d>' % i
+ elif which == 'file':
+ command = interpreter.string
+ name = '<file:%d (%s)>' % (i, thing)
+ target = '%s{execfile("""%s""")}' % (_prefix, thing)
+ elif which == 'import':
+ command = interpreter.string
+ name = '<import:%d>' % i
+ target = '%s{import %s}' % (_prefix, thing)
+ else:
+ assert 0
+ interpreter.wrap(command, (target, name))
+ i = i + 1
+ # Now process the primary file.
+ interpreter.ready()
+ if filename == '-':
+ if not _interactive:
+ name = '<stdin>'
+ path = ''
+ file = sys.stdin
+ else:
+ name, file = None, None
+ else:
+ name = filename
+ file = theSubsystem.open(filename, 'r')
+ path = os.path.split(filename)[0]
+ if _relativePath:
+ sys.path.insert(0, path)
+ if file is not None:
+ if _binary < 0:
+ interpreter.wrap(interpreter.file, (file, name))
+ else:
+ chunkSize = _binary
+ interpreter.wrap(interpreter.binary, (file, name, chunkSize))
+ # If we're supposed to go interactive afterwards, do it.
+ if _interactive:
+ interpreter.interact()
+ finally:
+ interpreter.shutdown()
+ # Finally, if we should pause at the end, do it.
+ if _pauseAtEnd:
+ try:
+ raw_input()
+ except EOFError:
+ pass
+
+def main():
+ invoke(sys.argv[1:])
+
+if __name__ == '__main__': main()
diff --git a/py-bin/lib/jon/__init__.py b/py-bin/lib/jon/__init__.py
new file mode 100644
index 0000000..1f42b03
--- /dev/null
+++ b/py-bin/lib/jon/__init__.py
@@ -0,0 +1,3 @@
+# $Id: __init__.py,v 1.7 2004/03/04 17:29:18 jribbens Exp $
+
+__version__ = "0.06"
diff --git a/py-bin/lib/jon/cgi.py b/py-bin/lib/jon/cgi.py
new file mode 100644
index 0000000..a1771c4
--- /dev/null
+++ b/py-bin/lib/jon/cgi.py
@@ -0,0 +1,673 @@
+# $Id: cgi.py,v 1.31 2004/03/24 12:14:31 jribbens Exp $
+
+import sys, re, os, Cookie, errno
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+"""Object-oriented CGI interface."""
+
+
+class Error(Exception):
+ """The base class for all exceptions thrown by this module."""
+ pass
+
+class SequencingError(Error):
+ """The exception thrown when functions are called out of order."""
+ """
+ For example, if you try to call a function altering the headers of your
+ output when the headers have already been sent.
+ """
+ pass
+
+
+_url_encre = re.compile(r"[^A-Za-z0-9_.!~*()-]") # RFC 2396 section 2.3
+_url_decre = re.compile(r"%([0-9A-Fa-f]{2})")
+_html_encre = re.compile("[&<>\"'+]")
+# '+' is encoded because it is special in UTF-7, which the browser may select
+# automatically if the content-type header does not specify the character
+# encoding. This is paranoia and is not bulletproof, but it does no harm. See
+# section 4 of www.microsoft.com/technet/security/news/csoverv.mspx
+_html_encodes = { "&": "&amp;", "<": "&lt;", ">": "&gt;", "\"": "&quot;",
+ "'": "&#39;", "+": "&#43;" }
+
+def html_encode(raw):
+ """Return the string parameter HTML-encoded."""
+ """
+ Specifically, the following characters are encoded as entities:
+ & < > " ' +
+ """
+ if not isinstance(raw, unicode):
+ raw = str(raw)
+ return re.sub(_html_encre, lambda m: _html_encodes[m.group(0)], raw)
+
+def url_encode(raw):
+ """Return the string parameter URL-encoded."""
+ if not isinstance(raw, unicode):
+ raw = str(raw)
+ return re.sub(_url_encre, lambda m: "%%%02X" % ord(m.group(0)), raw)
+
+def url_decode(enc):
+ """Return the string parameter URL-decoded (including '+' -> ' ')."""
+ s = enc.replace("+", " ")
+ return re.sub(_url_decre, lambda m: chr(int(m.group(1), 16)), s)
+
+
+__UNDEF__ = []
+
+def _lookup(name, frame, locls):
+ if name in locls:
+ return "local", locls[name]
+ if name in frame.f_globals:
+ return "global", frame.f_globals[name]
+ if "__builtins__" in frame.f_globals and \
+ hasattr(frame.f_globals["__builtins__"], name):
+ return "builtin", getattr(frame.f_globals["__builtins__"], name)
+ return None, __UNDEF__
+
+
+def _scanvars(reader, frame, locls):
+ import tokenize, keyword
+ vrs = []
+ lasttoken = None
+ parent = None
+ prefix = ""
+ for ttype, token, start, end, line in tokenize.generate_tokens(reader):
+ if ttype == tokenize.NEWLINE:
+ break
+ elif ttype == tokenize.NAME and token not in keyword.kwlist:
+ if lasttoken == ".":
+ if parent is not __UNDEF__:
+ value = getattr(parent, token, __UNDEF__)
+ vrs.append((prefix + token, prefix, value))
+ else:
+ (where, value) = _lookup(token, frame, locls)
+ vrs.append((token, where, value))
+ elif token == ".":
+ prefix += lasttoken + "."
+ parent = value
+ else:
+ parent = None
+ prefix = ""
+ lasttoken = token
+ return vrs
+
+
+def _tb_encode(s):
+ return html_encode(s).replace(" ", "&nbsp;")
+
+
+def traceback(req, html=0):
+ import traceback, time, types, linecache, inspect, repr
+ repr = repr.Repr()
+ repr.maxdict = 10
+ repr.maxlist = 10
+ repr.maxtuple = 10
+ repr.maxother = 200
+ repr.maxstring = 200
+ repr = repr.repr
+ (etype, evalue, etb) = sys.exc_info()
+ if type(etype) is types.ClassType:
+ etype = etype.__name__
+ if html:
+ try:
+ req.clear_headers()
+ req.clear_output()
+ req.set_header("Content-Type", "text/html; charset=iso-8859-1")
+ except SequencingError:
+ req.write("</font></font></font></script></object></blockquote></pre>"
+ "</table></table></table></table></table></table></font></font>")
+ req.write("""\
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head><title>jonpy traceback: %s</title>
+<style type="text/css"><!--
+BODY { background-color: #f0f0f8; font-family: helveta, arial, sans-serif }
+.tb_head { background-color: #6622aa; color: #ffffff }
+.tb_title { font-size: x-large }
+.tb_frame { background-color: #d8bbff }
+.tb_lineno { font-size: smaller }
+.tb_codehigh { background-color: #ffccee }
+.tb_code { color: #909090 }
+.tb_dump { color: #909090; font-size: smaller }
+--></style>
+</head><body>
+<table width="100%%" cellspacing="0" cellpadding="0" border="0">
+<tr class="tb_head">
+<td valign="bottom" class="tb_title">&nbsp;<br /><strong>%s</strong></td>
+<td align="right" valign="bottom">%s<br />%s</td></tr></table>
+<p>A problem occurred in a Python script. Here is the sequence of
+function calls leading up to the error, with the most recent first.</p>
+""" % (_tb_encode(etype), _tb_encode(etype),
+ "Python %s: %s" % (sys.version.split()[0], sys.executable),
+ time.ctime(time.time())))
+ req.error("jonpy error: %s at %s\n" % (etype, time.ctime(time.time())))
+ # this code adapted from the standard cgitb module
+ # unfortunately we cannot use that module directly,
+ # mainly because it won't allow us to output to the log
+ if html:
+ req.write("<p><strong>%s</strong>: %s" % (_tb_encode(etype),
+ _tb_encode(evalue)))
+ req.error("%s: %s\n" % (etype, evalue))
+ #if type(evalue) is types.InstanceType:
+ # for name in dir(evalue):
+ # if html:
+ # req.write("\n<br /><tt>&nbsp;&nbsp;&nbsp;&nbsp;</tt>%s&nbsp;= %s" %
+ # (_tb_encode(name), _tb_encode(repr(getattr(evalue, name)))))
+ # req.error(" %s = %s\n" % (name, repr(getattr(evalue, name))))
+ if html:
+ req.write("</p>\n")
+ frames = []
+ records = inspect.getinnerframes(etb, 7)
+ records.reverse()
+ for frame, fn, lnum, func, lines, index in records:
+ if html:
+ req.write("""\
+<table width="100%" cellspacing="0" cellpadding="0" border="0">""")
+ fn = fn and os.path.abspath(fn) or "?"
+ args, varargs, varkw, locls = inspect.getargvalues(frame)
+ if func != "?":
+ fav = inspect.formatargvalues(args, varargs, varkw, locls)
+ if html:
+ req.write("<tr><td class=\"tb_frame\">%s in <strong>%s</strong>%s</td>"
+ "</tr>\n" % (_tb_encode(fn), _tb_encode(func), _tb_encode(fav)))
+ req.error("%s in %s%s\n" % (fn, func, fav))
+ else:
+ if html:
+ req.write("<tr><td class=\"tb_head\">%s</td></tr>\n" %
+ (_tb_encode(fn),))
+ req.error("%s\n" % (fn,))
+ highlight = {}
+ def reader(lnum=[lnum]):
+ highlight[lnum[0]] = 1
+ try:
+ return linecache.getline(fn, lnum[0])
+ finally:
+ lnum[0] += 1
+ vrs = _scanvars(reader, frame, locls)
+ if index is not None:
+ i = lnum - index
+ for line in lines:
+ if html:
+ if i in highlight:
+ style = "tb_codehigh"
+ else:
+ style = "tb_code"
+ req.write("<tr><td class=\"%s\"><code><span class=\"tb_lineno\">"
+ "%s</span>&nbsp;%s</code></td></tr>\n" %
+ (style, "&nbsp;" * (5 - len(str(i))) + str(i), _tb_encode(line)))
+ req.error("%s %s" % (" " * (5-len(str(i))) + str(i), line))
+ i += 1
+ done = {}
+ dump = []
+ htdump = []
+ for name, where, value in vrs:
+ if name in done:
+ continue
+ done[name] = 1
+ if value is not __UNDEF__:
+ if where == "global":
+ dump.append("global %s = %s" % (name, repr(value)))
+ htdump.append("<em>global</em> <strong>%s</strong>&nbsp;= %s" %
+ (_tb_encode(name), _tb_encode(repr(value))))
+ elif where == "builtin":
+ dump.append("builtin %s = %s" % (name, repr(value)))
+ htdump.append("<em>builtin</em> <strong>%s</strong>&nbsp;= %s" %
+ (_tb_encode(name), _tb_encode(repr(value))))
+ elif where == "local":
+ dump.append("%s = %s" % (name, repr(value)))
+ htdump.append("<strong>%s</strong>&nbsp;= %s" %
+ (_tb_encode(name), _tb_encode(repr(value))))
+ else:
+ dump.append("%s%s = %s" % (where, name.split(".")[-1], repr(value)))
+ htdump.append("%s<strong>%s</strong>&nbsp;= %s" %
+ (_tb_encode(where), _tb_encode(name.split(".")[-1]),
+ _tb_encode(repr(value))))
+ else:
+ dump.append("%s undefined" % (name,))
+ htdump.append("%s <em>undefined</em>" % (_tb_encode(name,)))
+ if html:
+ req.write("<tr><td class=\"tb_dump\">%s</td></tr>\n" %
+ (", ".join(htdump),))
+ req.error(", ".join(dump) + "\n")
+ if html:
+ req.write("</table>\n")
+ if html:
+ req.write("</body></html>\n")
+ linecache.clearcache()
+
+
+class Request(object):
+ """All the information about a CGI-style request, including how to respond."""
+ """Headers are buffered in a list before being sent. They are either sent
+ on request, or when the first part of the body is sent. If requested, the
+ body output can be buffered as well."""
+
+ def __init__(self, handler_type):
+ """Create a Request object which uses handler_type as its handler."""
+ """An object of type handler_type, which should be a subclass of
+ Handler, will be used to handle requests."""
+ self._handler_type = handler_type
+
+ def _init(self):
+ self._doneHeaders = 0
+ self._headers = []
+ self._bufferOutput = 1
+ self._output = StringIO.StringIO()
+ self._pos = 0
+ self.closed = 0
+ try:
+ del self.params
+ except AttributeError:
+ pass
+ self.cookies = Cookie.SimpleCookie()
+ if self.environ.has_key("HTTP_COOKIE"):
+ self.cookies.load(self.environ["HTTP_COOKIE"])
+ self.aborted = 0
+ self.set_header("Content-Type", "text/html; charset=iso-8859-1")
+
+ def __getattr__(self, name):
+ if name == "params":
+ self.params = {}
+ self._read_cgi_data(self.environ, self.stdin)
+ return self.__dict__["params"]
+ raise AttributeError, "%s instance has no attribute %s" % \
+ (self.__class__.__name__, `name`)
+
+ def close(self):
+ """Closes the output stream."""
+ if not self.closed:
+ self.flush()
+ self._close()
+ self.closed = 1
+
+ def _check_open(self):
+ if self.closed:
+ raise ValueError, "I/O operation on closed file"
+
+ def output_headers(self):
+ """Output the list of headers."""
+ self._check_open()
+ if self._doneHeaders:
+ raise SequencingError, "output_headers() called twice"
+ for pair in self._headers:
+ self._write("%s: %s\r\n" % pair)
+ self._write("\r\n")
+ self._doneHeaders = 1
+
+ def clear_headers(self):
+ """Clear the list of headers."""
+ self._check_open()
+ if self._doneHeaders:
+ raise SequencingError, "cannot clear_headers() after output_headers()"
+ self._headers = []
+
+ def add_header(self, hdr, val):
+ """Add a header to the list of headers."""
+ self._check_open()
+ if self._doneHeaders:
+ raise SequencingError, \
+ "cannot add_header(%s) after output_headers()" % `hdr`
+ self._headers.append((hdr, val))
+
+ def set_header(self, hdr, val):
+ """Add a header to the list of headers, replacing any existing values."""
+ self._check_open()
+ if self._doneHeaders:
+ raise SequencingError, \
+ "cannot set_header(%s) after output_headers()" % `hdr`
+ self.del_header(hdr)
+ self._headers.append((hdr, val))
+
+ def get_header(self, hdr, index=0):
+ """Retrieve a header from the list of headers."""
+ i = 0
+ hdr = hdr.lower()
+ for pair in self._headers:
+ if pair[0].lower() == hdr:
+ if i == index:
+ return pair[1]
+ i += 1
+ return None
+
+ def del_header(self, hdr):
+ """Removes all values for a header from the list of headers."""
+ self._check_open()
+ if self._doneHeaders:
+ raise SequencingError, \
+ "cannot del_header(%s) after output_headers()" % `hdr`
+ hdr = hdr.lower()
+ while 1:
+ for s in self._headers:
+ if s[0].lower() == hdr:
+ self._headers.remove(s)
+ break
+ else:
+ break
+
+ def set_buffering(self, f):
+ """Specifies whether or not body output is buffered."""
+ self._check_open()
+ if self._output.tell() > 0 and not f:
+ self.flush()
+ self._bufferOutput = f
+
+ def flush(self):
+ """Flushes the body output."""
+ self._check_open()
+ if not self._doneHeaders:
+ self.output_headers()
+ self._write(self._output.getvalue())
+ self._pos += self._output.tell()
+ self._output.seek(0, 0)
+ self._output.truncate()
+ self._flush()
+
+ def clear_output(self):
+ """Discards the contents of the body output buffer."""
+ self._check_open()
+ if not self._bufferOutput:
+ raise SequencingError, "cannot clear output when not buffering"
+ self._output.seek(0, 0)
+ self._output.truncate()
+
+ def error(self, s):
+ """Records an error message from the program."""
+ """The output is logged or otherwise stored on the server. It does not
+ go to the client.
+
+ Must be overridden by the sub-class."""
+ raise NotImplementedError, "error must be overridden"
+
+ def _write(self, s):
+ """Sends some data to the client."""
+ """Must be overridden by the sub-class."""
+ raise NotImplementedError, "_write must be overridden"
+
+ def _flush(self):
+ """Flushes data to the client."""
+ """May be overridden by the sub-class."""
+ pass
+
+ def _close(self):
+ """Closes the output stream."""
+ """May be overridden by the sub-class."""
+ pass
+
+ def write(self, s):
+ """Sends some data to the client."""
+ self._check_open()
+ s = str(s)
+ if self._bufferOutput:
+ self._output.write(s)
+ else:
+ if not self._doneHeaders:
+ self.output_headers()
+ self._pos += len(s)
+ self._write(s)
+
+ def tell(self):
+ return self._pos + self._output.tell()
+
+ def seek(self, offset, whence=0):
+ self._check_open()
+ currentpos = self._pos + self._output.tell()
+ currentlen = self._pos + len(self._output.getvalue())
+ if whence == 0:
+ newpos = offset
+ elif whence == 1:
+ newpos = currentpos + offset
+ elif whence == 2:
+ newpos = currentlen + offset
+ else:
+ raise ValueError, "Bad 'whence' argument to seek()"
+ if newpos == currentpos:
+ return
+ elif newpos < self._pos:
+ raise ValueError, "Cannot seek backwards into already-sent data"
+ elif newpos <= currentlen:
+ self._output.seek(newpos - self._pos)
+ else:
+ if self._bufferOutput:
+ self._output.seek(newpos - self._pos)
+ else:
+ self._write("\0" * (newpos - self._pos))
+
+ def _mergevars(self, encoded):
+ """Parse variable-value pairs from a URL-encoded string."""
+ """Extract the variable-value pairs from the URL-encoded input string and
+ merge them into the output dictionary. Variable-value pairs are separated
+ from each other by the '&' character. Missing values are allowed.
+
+ If the variable name ends with a '*' character, then the value that is
+ placed in the dictionary will be a list. This is useful for multiple-value
+ fields."""
+ for pair in encoded.split("&"):
+ if pair == "":
+ continue
+ nameval = pair.split("=", 1)
+ name = url_decode(nameval[0])
+ if len(nameval) > 1:
+ val = url_decode(nameval[1])
+ else:
+ val = None
+ if name.endswith("!") or name.endswith("!*"):
+ continue
+ if name.endswith("*"):
+ if self.params.has_key(name):
+ self.params[name].append(val)
+ else:
+ self.params[name] = [val]
+ else:
+ self.params[name] = val
+
+ def _mergemime(self, contenttype, encoded):
+ """Parses variable-value pairs from a MIME-encoded input stream."""
+ """Extract the variable-value pairs from the MIME-encoded input file and
+ merge them into the output dictionary.
+
+ If the variable name ends with a '*' character, then the value that is
+ placed in the dictionary will be a list. This is useful for multiple-value
+ fields. If the variable name ends with a '!' character (before the '*' if
+ present) then the value will be a mime.Entity object."""
+ import mime
+ headers = "Content-Type: %s\n" % contenttype
+ for entity in mime.Entity(encoded.read(), mime=1, headers=headers).entities:
+ if not entity.content_disposition:
+ continue
+ if entity.content_disposition[0] != 'form-data':
+ continue
+ name = entity.content_disposition[1].get("name")
+ if name[-1:] == "*":
+ if self.params.has_key(name):
+ if name[-2:-1] == "!":
+ self.params[name].append(entity)
+ else:
+ self.params[name].append(entity.body)
+ else:
+ if name[-2:-1] == "!":
+ self.params[name] = [entity]
+ else:
+ self.params[name] = [entity.body]
+ elif name[-1:] == "!":
+ self.params[name] = entity
+ else:
+ self.params[name] = entity.body
+
+ def _read_cgi_data(self, environ, inf):
+ """Read input data from the client and set up the object attributes."""
+ if environ.has_key("QUERY_STRING"):
+ self._mergevars(environ["QUERY_STRING"])
+ if environ.get("REQUEST_METHOD") == "POST":
+ if environ.get("CONTENT_TYPE", "").startswith("multipart/form-data"):
+ self._mergemime(environ["CONTENT_TYPE"], inf)
+ else:
+ self._mergevars(inf.read(int(environ.get("CONTENT_LENGTH", "-1"))))
+
+ def traceback(self):
+ traceback(self)
+ try:
+ self.clear_headers()
+ self.clear_output()
+ self.set_header("Content-Type", "text/html; charset=iso-8859-1")
+ except SequencingError:
+ pass
+ self.write("""\
+<html><head><title>Error</title></head>
+<body><h1>Error</h1>
+<p>Sorry, an error occurred. Please try again later.</p>
+</body></html>""")
+
+
+class GZipMixIn(object):
+ def _init(self):
+ self._gzip = None
+ self._gzip_level = 6
+ super(GZipMixIn, self)._init()
+
+ def _close(self):
+ if self._gzip:
+ import struct
+ super(GZipMixIn, self)._write(self._gzip.flush(self._gzip_zlib.Z_FINISH))
+ super(GZipMixIn, self)._write(
+ struct.pack("<II", self._gzip_crc, self._gzip_length))
+ super(GZipMixIn, self)._flush()
+ self._gzip = None
+ super(GZipMixIn, self)._close()
+
+ def gzip_level(self, level=6):
+ """Enable/disable gzip output compression."""
+ if self._gzip_level == level:
+ return
+ if self._doneHeaders:
+ raise SequencingError, "Cannot adjust compression - headers already sent"
+ self._gzip_level = level
+
+ def _write(self, s):
+ if not self._gzip:
+ super(GZipMixIn, self)._write(s)
+ return
+ self._gzip_crc = self._gzip_zlib.crc32(s, self._gzip_crc)
+ self._gzip_length += len(s)
+ super(GZipMixIn, self)._write(self._gzip.compress(s))
+
+ def output_headers(self):
+ if self._gzip_level == 0:
+ super(GZipMixIn, self).output_headers()
+ return
+ gzip_ok = 0
+ if self.environ.has_key("HTTP_ACCEPT_ENCODING"):
+ encodings = [[a.strip() for a in x.split(";", 1)]
+ for x in self.environ["HTTP_ACCEPT_ENCODING"].split(",")]
+ for encoding in encodings:
+ if encoding[0].lower() == "gzip":
+ if len(encoding) == 1:
+ gzip_ok = 1
+ break
+ else:
+ q = [x.strip() for x in encoding[1].split("=")]
+ if len(q) == 2 and q[0].lower() == "q" and q[1] != "0":
+ gzip_ok = 1
+ break
+ if gzip_ok:
+ try:
+ import zlib
+ encoding = "gzip"
+ encoding = self.get_header("Content-Encoding")
+ if encoding is None:
+ encoding = "gzip"
+ else:
+ encoding += ", gzip"
+ self.set_header("Content-Encoding", encoding)
+ self.del_header("Content-Length")
+ super(GZipMixIn, self).output_headers()
+ self._gzip = zlib.compressobj(self._gzip_level, 8, -15)
+ self._gzip_zlib = zlib
+ self._gzip_crc = self._gzip_length = 0
+ super(GZipMixIn, self)._write(
+ "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03")
+ return
+ except ImportError:
+ pass
+ super(GZipMixIn, self).output_headers()
+
+ def _flush(self):
+ if self._gzip:
+ super(GZipMixIn, self)._write(
+ self._gzip.flush(self._gzip_zlib.Z_SYNC_FLUSH))
+ super(GZipMixIn, self)._flush()
+
+
+class CGIRequest(Request):
+ """An implementation of Request which uses the standard CGI interface."""
+
+ def _init(self):
+ self.__out = sys.stdout
+ self.__err = sys.stderr
+ self.environ = os.environ
+ self.stdin = sys.stdin
+ super(CGIRequest, self)._init()
+
+ def process(self):
+ """Read the CGI input and create and run a handler to handle the request."""
+ self._init()
+ try:
+ handler = self._handler_type()
+ except:
+ self.traceback()
+ else:
+ try:
+ handler.process(self)
+ except:
+ handler.traceback(self)
+ self.close()
+
+ def error(self, s):
+ self.__err.write(s)
+
+ def _write(self, s):
+ if not self.aborted:
+ try:
+ self.__out.write(s)
+ except IOError, x:
+ # Ignore EPIPE, caused by the browser having gone away
+ if x[0] != errno.EPIPE:
+ raise
+ self.aborted = 1
+
+ def _flush(self):
+ if not self.aborted:
+ try:
+ self.__out.flush()
+ except IOError, x:
+ # Ignore EPIPE, caused by the browser having gone away
+ if x[0] != errno.EPIPE:
+ raise
+ self.aborted = 1
+
+
+class GZipCGIRequest(GZipMixIn, CGIRequest):
+ pass
+
+
+class Handler(object):
+ """Handle a request."""
+ def process(self, req):
+ """Handle a request. req is a Request object."""
+ raise NotImplementedError, "handler process function must be overridden"
+
+ def traceback(self, req):
+ """Display a traceback, req is a Request object."""
+ req.traceback()
+
+
+class DebugHandlerMixIn(object):
+ def traceback(self, req):
+ """Display a traceback, req is a Request object."""
+ traceback(req, html=1)
+
+
+class DebugHandler(DebugHandlerMixIn, Handler):
+ pass
diff --git a/py-bin/lib/jon/session.py b/py-bin/lib/jon/session.py
new file mode 100644
index 0000000..7a253ab
--- /dev/null
+++ b/py-bin/lib/jon/session.py
@@ -0,0 +1,209 @@
+# $Id: session.py,v 1.9 2003/01/02 23:24:44 jribbens Exp $
+
+import time, hmac, sha, Cookie, re, random, os, errno, fcntl
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+
+class Error(Exception):
+ pass
+
+
+class Session(dict):
+ def _make_hash(self, sid, secret):
+ """Create a hash for 'sid'
+
+ This function may be overridden by subclasses."""
+ return hmac.new(secret, sid, sha).hexdigest()[:8]
+
+ def _create(self, secret):
+ """Create a new session ID and, optionally hash
+
+ This function must insert the new session ID (which must be 8 hexadecimal
+ characters) into self["id"].
+
+ It may optionally insert the hash into self["hash"]. If it doesn't, then
+ _make_hash will automatically be called later.
+
+ This function may be overridden by subclasses.
+ """
+ rnd = str(time.time()) + str(random.random()) + \
+ str(self._req.environ.get("UNIQUE_ID"))
+ self["id"] = sha.new(rnd).hexdigest()[:8]
+
+ def _load(self):
+ """Load the session dictionary from somewhere
+
+ This function may be overridden by subclasses.
+ It should return 1 if the load was successful, or 0 if the session could
+ not be found. Any other type of error should raise an exception as usual."""
+ return 1
+
+ def save(self):
+ """Save the session dictionary to somewhere
+
+ This function may be overridden by subclasses."""
+ pass
+
+ def tidy():
+ pass
+ tidy = staticmethod(tidy)
+
+ def __init__(self, req, secret, cookie="jonsid", url=0, root="",
+ referer=None, sid=None, shash=None):
+ dict.__init__(self)
+ self["id"] = None
+ self._req = req
+ self.relocated = 0
+ self.new = 0
+
+ # try and determine existing session id
+
+ if sid is not None:
+ self["id"] = sid
+ if shash is None:
+ self["hash"] = self._make_hash(self["id"], secret)
+ else:
+ self["hash"] = shash
+ if self["hash"] != self._make_hash(self["id"], secret):
+ self["id"] = None
+
+ if cookie and self._req.cookies.has_key(cookie):
+ self["id"] = self._req.cookies[cookie].value[:8]
+ self["hash"] = self._req.cookies[cookie].value[8:]
+ if self["hash"] != self._make_hash(self["id"], secret):
+ self["id"] = None
+
+ if url:
+ for i in xrange(1, 4):
+ requrl = self._req.environ.get("REDIRECT_" * i + "SESSION")
+ if requrl:
+ break
+ if requrl and self["id"] is None:
+ self["id"] = requrl[:8]
+ self["hash"] = requrl[8:]
+ if self["hash"] != self._make_hash(self["id"], secret):
+ self["id"] = None
+
+ # check the session
+
+ if referer:
+ if self._req.environ.has_key("HTTP_REFERER"):
+ if self._req.environ["HTTP_REFERER"].find(referer) == -1:
+ self["id"] = None
+
+ # try and load the session
+
+ if self["id"] is not None:
+ if not self._load():
+ self["id"] = None
+
+ # if no session was available and loaded, create a new one
+
+ if self["id"] is None:
+ if self.has_key("hash"):
+ del self["hash"]
+ self.created = time.time()
+ self.new = 1
+ self._create(secret)
+ if not self.has_key("hash"):
+ self["hash"] = self._make_hash(self["id"], secret)
+ if cookie:
+ c = Cookie.SimpleCookie()
+ c[cookie] = self["id"] + self["hash"]
+ c[cookie]["path"] = root + "/"
+ self._req.add_header("Set-Cookie", c[cookie].OutputString())
+
+ # if using url-based sessions, redirect if necessary
+
+ if url:
+ if not requrl or self["id"] != requrl[:8] or self["hash"] != requrl[8:]:
+ requrl = self._req.environ["REQUEST_URI"][len(root):]
+ requrl = re.sub("^/[A-Fa-f0-9]{16}/", "/", requrl)
+ self._req.add_header("Location", "http://" +
+ self._req.environ["SERVER_NAME"] + root + "/" + self["id"] +
+ self["hash"] + requrl)
+ self.relocated = 1
+ self.surl = root + "/" + self["id"] + self["hash"] + "/"
+
+class FileSession(Session):
+ def _create(self, secret):
+ while 1:
+ Session._create(self, secret)
+ try:
+ os.lstat("%s/%s" % (self.basedir, self["id"][:2]))
+ except OSError, x:
+ if x[0] == errno.ENOENT:
+ os.mkdir("%s/%s" % (self.basedir, self["id"][:2]), 0700)
+ try:
+ fd = os.open("%s/%s/%s" % (self.basedir, self["id"][:2],
+ self["id"][2:]), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0700)
+ except OSError, x:
+ if x[0] != errno.EEXIST:
+ raise
+ continue
+ f = os.fdopen(fd, "wb")
+ f.write("%d\n" % self.created)
+ pickle.dump({}, f, 1)
+ f.flush()
+ break
+
+ def _load(self):
+ try:
+ f = open("%s/%s/%s" % (self.basedir, self["id"][:2], self["id"][2:]),
+ "r+b")
+ except IOError, x:
+ if x[0] != errno.ENOENT:
+ raise
+ return 0
+ fcntl.lockf(f.fileno(), fcntl.LOCK_EX)
+ self.created = int(f.readline().strip())
+ self.update(pickle.load(f))
+ return 1
+
+ def save(self):
+ f = open("%s/%s/%s" % (self.basedir, self["id"][:2], self["id"][2:]), "r+b")
+ fcntl.lockf(f.fileno(), fcntl.LOCK_EX)
+ f.write("%d\n" % self.created)
+ pickle.dump(self.copy(), f, 1)
+ f.flush()
+ f.truncate()
+
+ def tidy(cls, max_idle=0, max_age=0, basedir=None):
+ if not max_idle and not max_age:
+ return
+ basedir = cls._find_basedir(basedir)
+ now = time.time()
+ for d in os.listdir(basedir):
+ if len(d) != 2 or not d.isalnum():
+ continue
+ for f in os.listdir("%s/%s" % (basedir, d)):
+ if len(f) != 6 or not f.isalnum():
+ continue
+ p = "%s/%s/%s" % (basedir, d, f)
+ if (max_idle and os.lstat(p).st_mtime < now - max_idle) or \
+ (max_age and int(open(p, "rb").readline().strip()) < now - max_age):
+ os.remove(p)
+ tidy = classmethod(tidy)
+
+ def _find_basedir(basedir):
+ if basedir is None:
+ basedir = os.environ.get("TMPDIR", "/tmp")
+ while basedir[-1] == "/":
+ basedir = basedir[:-1]
+ basedir = "%s/jon-sessions-%d" % (basedir, os.getuid())
+ try:
+ st = os.lstat(basedir)
+ if st[4] != os.getuid():
+ raise Error, "Sessions basedir is not owned by user %d" % os.getuid()
+ except OSError, x:
+ if x[0] == errno.ENOENT:
+ os.mkdir(basedir, 0700)
+ return basedir
+ _find_basedir = staticmethod(_find_basedir)
+
+ def __init__(self, req, secret, basedir=None, **kwargs):
+ self.basedir = self._find_basedir(basedir)
+ Session.__init__(self, req, secret, **kwargs)
diff --git a/py-bin/login.py b/py-bin/login.py
new file mode 100644
index 0000000..2fd85b3
--- /dev/null
+++ b/py-bin/login.py
@@ -0,0 +1,24 @@
+#login
+
+import config
+
+class LoginMixIn:
+ def login_form(self, req):
+ self.render_template(req, "login_form.em")
+ login_form.web_callable = True
+
+ def login_process(self, req):
+ email = req.params.get("email", "")
+ jabberpw = req.params.get("jabberpw", "")
+
+ success, status_or_user = self.jman.login(email, jabberpw)
+ if not success:
+ return self.failed_page(req, status_or_user)
+
+ self.redirect_to(req, config.script_url + "?cmd=setup_main")
+ login_process.web_callable = True
+
+ def failed_page(self, req, reason):
+ self.render_template(req, "login_fail.em", dict(reason=reason))
+ return None
+
diff --git a/py-bin/mail_auth.py b/py-bin/mail_auth.py
new file mode 100644
index 0000000..27607e8
--- /dev/null
+++ b/py-bin/mail_auth.py
@@ -0,0 +1,111 @@
+#mail authentication
+
+import smtplib, hmac, sha, re, time, logging
+from utils import empy_render
+import subprocess
+import config
+
+class MailAuthMixIn:
+ def mail_form(self, req):
+ self.render_template(req, "mail_form.em")
+ mail_form.web_callable = True
+
+ def mail_process(self, req):
+ email = req.params.get("email", "")
+
+ success, status_or_token = self.jman.generate_token(email)
+ if not success:
+ self.render_template(req, "mail_error.em", dict(reason=status_or_token))
+ return
+ token = status_or_token
+
+ if not self.__send_mail(email, token):
+ msg = "Unerwarteter Fehler beim Versenden des Mails."
+ self.render_template(req, "mail_trylater.em", dict(reason=msg))
+ return
+
+ success, status = self.jman.prepare_user(email, token)
+ if not success:
+ self.render_template(req, "mail_error.em", dict(reason=status))
+ return
+
+ msg = "Mail erfolgreich versandt."
+ self.render_template(req, "mail_success.em", dict(status=msg, email=email))
+ mail_process.web_callable = True
+
+ def mail_pw_form(self, req):
+ user_id = req.params.get("uid", "")
+ token = req.params.get("tok", "")
+ last_error = req.params.get("error", "")
+
+ token_ok, status_or_user = self.jman.validate_token(user_id, token)
+ if token_ok:
+ user_id = status_or_user.get_user_id()
+ self.render_template(req, "set_pw_form.em",
+ dict(user_id=user_id, error=last_error, command="mail_pw_process"))
+ else:
+ self.error_page(req, status_or_user)
+ mail_pw_form.web_callable = True
+
+ def mail_pw_process(self, req):
+ password = req.params.get("password", "")
+ password2 = req.params.get("password2", "")
+
+ ok, status = self.jman.is_acceptable_password(password, password2)
+ if not ok:
+ url = self.make_url([("cmd","mail_pw_form"), ("error", status)])
+ self.redirect_to(req, url)
+ return
+
+ ok, status_or_user = self.jman.activate_user(password)
+ if ok:
+ self.redirect_to(req, config.script_url + "?cmd=setup_main")
+ else:
+ self.error_page(req, status_or_user)
+ mail_pw_process.web_callable = True
+
+ def __send_mail(self, email, token):
+ date = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
+ url = self.make_url([("cmd","mail_pw_form"), ("uid",email), ("tok",token)])
+ ctx = dict(from_addr=config.mail_from_addr, to_addr=email,
+ date=date, reg_link=url)
+ message = empy_render("mail_message.em", context_raw = ctx)
+
+ if hasattr(config,"use_sendmail") and config.use_sendmail:
+ return self.__sendmail_send_mail(email, token, message)
+ else:
+ return self.__smtp_send_mail(email, token, message)
+
+ def __smtp_send_mail(self, email, token, message):
+ try:
+ server = smtplib.SMTP(config.smtp_server)
+ server.ehlo()
+ server.starttls()
+ server.ehlo()
+ if hasattr(config, "smtp_pass"):
+ server.login(config.smtp_user, config.smtp_pass)
+ server.sendmail(config.mail_from_addr, [email], message)
+ server.quit()
+ logging.info("Sent mail to %s." % email)
+ except Exception, e:
+ logging.error("Error sending mail to %s: %s" % (email, str(e)))
+ return False
+
+ return True
+
+ def __sendmail_send_mail(self, email, token, message):
+ try:
+ params = ["/usr/sbin/sendmail", "-f" + config.mail_from_addr, email]
+ p = subprocess.Popen(params, stdin=subprocess.PIPE)
+ p.communicate(message)
+ result = p.wait()
+ if result != 0:
+ logging.error("Error queuing mail for %s: return code %s" %
+ (email, str(result)))
+ return False
+ logging.info("Queued mail for %s." % email)
+ except Exception, e:
+ logging.error("Error queuing mail for %s: %s" % (email, str(e)))
+ return False
+ return True
+
diff --git a/py-bin/main.py b/py-bin/main.py
new file mode 100644
index 0000000..39294e0
--- /dev/null
+++ b/py-bin/main.py
@@ -0,0 +1,31 @@
+#main url mapper
+
+from utils import BasicHandler, process_request, set_logging_defaults
+from jabberman import JabberManager
+from login import LoginMixIn
+from mail_auth import MailAuthMixIn
+from setup import SetupMixIn
+
+set_logging_defaults()
+
+class MainHandler(BasicHandler, MailAuthMixIn, LoginMixIn, SetupMixIn):
+ def do_process(self, req):
+ command = req.params.get("cmd", "")
+
+ if command == "":
+ self.login_form(req)
+ else:
+ if hasattr(self, command):
+ method = getattr(self, command)
+ if hasattr(method, 'web_callable') and method.web_callable:
+ self.jman = JabberManager(self.session)
+ method(req)
+ else:
+ self.invalid_page(req)
+ else:
+ self.invalid_page(req)
+
+ def invalid_page(self, req):
+ self.error_page(req, "Ungueltiger Request.")
+
+process_request(MainHandler) \ No newline at end of file
diff --git a/py-bin/setup.py b/py-bin/setup.py
new file mode 100644
index 0000000..1978dd1
--- /dev/null
+++ b/py-bin/setup.py
@@ -0,0 +1,140 @@
+#jabber setup
+from lib.jon.cgi import html_encode, url_encode
+import config
+
+class SetupMixIn:
+ def setup_main(self, req):
+ user = self.__authenticate(req)
+ if not user:
+ return
+
+ paras = dict(user_id=user.get_user_id(), jabber_id=user.get_default_jabber_id())
+ acc_list = map(self.__get_delete_tuple, user.get_extra_account_list())
+ self.render_template(req, "setup_main.em", paras, dict(account_list=acc_list))
+ setup_main.web_callable = True
+
+ def __get_delete_tuple(self, account):
+ url = self.make_url([("cmd","delete_account_ask"), ("account",account)])
+ return (html_encode(account), url)
+
+ def set_pw_form(self, req):
+ user = self.__authenticate(req)
+ if not user:
+ return
+
+ last_error = req.params.get("error", "")
+ self.render_template(req, "set_pw_form.em",
+ dict(user_id=user.get_user_id(), error=last_error))
+ set_pw_form.web_callable = True
+
+ def set_pw_process(self, req):
+ user = self.__authenticate(req)
+ if not user:
+ return
+
+ password = req.params.get("password", "")
+ password2 = req.params.get("password2", "")
+
+ ok, status = self.jman.is_acceptable_password(password, password2)
+ if not ok:
+ url = self.make_url([("cmd","set_pw_form"), ("error", status)])
+ self.redirect_to(req, url)
+ return
+
+ self.jman.change_password(password)
+ self.__redirect_to_main(req)
+ set_pw_process.web_callable = True
+
+ def add_account_form(self, req):
+ user = self.__authenticate(req)
+ if not user:
+ return
+
+ last_err = req.params.get("error", "")
+ domains = config.extra_domains
+
+ self.render_template(req, "add_account_form.em",
+ dict(user_id=user.get_user_id(), domains=domains, error=last_err))
+ add_account_form.web_callable = True
+
+ def add_account_process(self, req):
+ user = self.__authenticate(req)
+ if not user:
+ return
+
+ domain = req.params.get("domain", "")
+ account = req.params.get("name", "") + "@" + domain
+
+ if domain not in config.extra_domains:
+ self.error_page(req, "Zugriff verweigert.")
+ return
+
+ ok, status = self.jman.add_account(account)
+ if not ok:
+ url = self.make_url([("cmd","add_account_form"), ("error", status)])
+ self.redirect_to(req, url)
+ return
+
+ self.__redirect_to_main(req)
+ add_account_process.web_callable = True
+
+ def delete_account_ask(self, req):
+ user = self.__authenticate(req)
+ if not user:
+ return
+
+ account = req.params.get("account", "")
+
+ raw = dict(account_urlenc = url_encode(account))
+ self.render_template(req, "delete_account_ask.em",
+ dict(account=account, user_id=user.get_user_id()), raw)
+ delete_account_ask.web_callable = True
+
+ def delete_account_process(self, req):
+ user = self.__authenticate(req)
+ if not user:
+ return
+
+ account = req.params.get("account", "")
+
+ ok, status = self.jman.remove_account(account)
+ if not ok:
+ self.error_page(req, status)
+ return
+
+ self.__redirect_to_main(req)
+ delete_account_process.web_callable = True
+
+ def help(self, req):
+ user = self.__authenticate(req)
+ if not user:
+ return
+
+ self.render_template(req, "setup_help.em",
+ dict(user_id=user.get_user_id(),jabber_id=user.get_default_jabber_id()))
+ help.web_callable = True
+
+ def logout(self, req):
+ user = self.__authenticate(req)
+ if not user:
+ return
+
+ self.jman.logout()
+ self.render_template(req, "logged_out.em")
+ logout.web_callable = True
+
+ def test(self, req):
+ ctx = dict(user_id="alice@immerda.ch")
+ self.render_template(req, "test.em", ctx)
+ test.web_callable = True
+
+ def __redirect_to_main(self, req):
+ self.redirect_to(req, self.make_url([("cmd","setup_main")]))
+
+ def __authenticate(self, req):
+ ok, status_or_user = self.jman.authenticate()
+ if not ok:
+ self.redirect_to(req, config.script_url)
+ return None
+ return status_or_user
+ \ No newline at end of file
diff --git a/py-bin/templates/add_account_form.em b/py-bin/templates/add_account_form.em
new file mode 100644
index 0000000..ae88204
--- /dev/null
+++ b/py-bin/templates/add_account_form.em
@@ -0,0 +1,17 @@
+@{em_inherit = "jman_setup_base.em"}
+@{title = "Jabber Konto Hinzufügen"}
+
+<form method="post" action="main.py?cmd=add_account_process">
+ <p>Name: <input type="text" name="name"/></p>
+ <p>Domain:
+ <select name="domain">
+ @[for domain in domains]
+ <option value="@domain">@domain</option>
+ @[end for]
+ </select>
+ </p>
+ <p><input type="reset" value="Zurücksetzen"/>
+ <input type="submit" name="submitted" value="Erstellen"/></p>
+</form>
+
+<p><a href="main.py?cmd=setup_main">Zurück</a></p>
diff --git a/py-bin/templates/delete_account_ask.em b/py-bin/templates/delete_account_ask.em
new file mode 100644
index 0000000..d28fb6a
--- /dev/null
+++ b/py-bin/templates/delete_account_ask.em
@@ -0,0 +1,8 @@
+@{em_inherit = "jman_setup_base.em"}
+@{title = "Jabber Konto Löschen"}
+
+<p>Jabber Konto: @account</p>
+
+<p><a href="main.py?cmd=setup_main">Abbrechen</a>
+ | <a href="main.py?cmd=delete_account_process&account=@account_urlenc">Löschen</a>
+</p>
diff --git a/py-bin/templates/error.em b/py-bin/templates/error.em
new file mode 100644
index 0000000..850e13e
--- /dev/null
+++ b/py-bin/templates/error.em
@@ -0,0 +1,13 @@
+@{em_inherit = "jman_base.em"}
+@{title = "Schwerer Fehler"}
+
+<div class="carbonbar">
+ <h2>Unbehebbarer Fehler aufgetreten</h2>
+</div>
+
+<div id="content">
+ <p>Fehlermeldung: <b>@message</b></p>
+ <p>Bitte wende dich an den technischen Support:
+ <a href="mailto:jabber AT immerda.ch"> jabber AT immerda.ch</a> (AT durch @@ ersetzen)
+ </p>
+</div> \ No newline at end of file
diff --git a/py-bin/templates/jman_base.em b/py-bin/templates/jman_base.em
new file mode 100644
index 0000000..35494a4
--- /dev/null
+++ b/py-bin/templates/jman_base.em
@@ -0,0 +1,94 @@
+<html>
+<head>
+<style type="text/css"><!--
+body {
+ font-family:Sans-serif;
+ font-size:14px;
+ background-color:#000000;
+ color:#ffffff;
+ text-align:center;
+ margin:0px;
+ padding:0px;
+}
+
+h1 {font-size:24px; margin:0px;}
+h2 {font-size:18px; margin:0px;}
+h3 {font-size:16px; margin:0px;}
+p {margin: 6px 0px;}
+b {
+ color: #cc1f1f;
+}
+a:link, a:visited, a:active, a:hover {
+ text-decoration:underline;
+ font-weight:bold;
+ color:#ffA900;
+}
+a:active, a:hover {
+ color:ffdd00;
+ text-shadow: #ffA900 1px 1px 6px;
+}
+
+#wholepage {
+ width: 840px;
+ margin: 0px auto;
+ text-align: left;
+}
+#maincolumn {
+ width:640px;
+ float:left;
+}
+#leftshadow, #rightshadow {
+ width: 100px;
+ height: 100%;
+ float: left;
+}
+#leftshadow {background: url(images/left-sm.png) repeat-y;}
+#rightshadow {background: url(images/right-sm.png) repeat-y;}
+
+#title {padding:5px 5px;}
+#logo {
+ float: right;
+ width: 180px;
+ height: 75px;
+ padding-right: 5px;
+}
+
+.carbonbar {
+ border-top: 2px solid #cc0000;
+ border-bottom: 2px solid #cc0000;
+ padding: 3px 5px;
+ clear: right;
+ background-image: url(images/carbon.png);
+}
+#userbar {
+ padding-top: 3px;
+ float: right;
+}
+#bottombar {text-align: center;}
+
+#content {
+ padding: 5px;
+ clear: right;
+}
+//--></style>
+<title>Jabber Verwaltung - @title</title>
+</head>
+
+<body>
+ <div id="wholepage">
+ <div id="leftshadow"></div>
+ <div id="maincolumn">
+ <img id="logo" src="images/jabber_logo.png" alt="Logo" />
+ <h1 id="title">Jabber Verwaltung</h1>
+
+ @em_child_content
+
+ <div id="bottombar" class="carbonbar">
+ Copyleft (<a href="http://creativecommons.org" target="_blank">cc</a>) 2007
+ <a href="http://www.immerda.ch" target="_blank">Immerda Projekt</a>
+ </div>
+ </div>
+ <div id="rightshadow"></div>
+ </div>
+</body>
+</html> \ No newline at end of file
diff --git a/py-bin/templates/jman_setup_base.em b/py-bin/templates/jman_setup_base.em
new file mode 100644
index 0000000..329c3b3
--- /dev/null
+++ b/py-bin/templates/jman_setup_base.em
@@ -0,0 +1,18 @@
+@{em_inherit = "jman_base.em"}
+
+<div class="carbonbar">
+ <div id="userbar">
+ @[if "user_id" in locals()]
+ <i>@user_id</i>&nbsp;&nbsp;|&nbsp;
+ <a href="main.py?cmd=set_pw_form">Passwort ändern</a>&nbsp;&nbsp;|&nbsp;
+ <a href="main.py?cmd=logout">Ausloggen</a>
+ @[end if]
+ </div>
+ <h2>@title</h2>
+</div>
+
+<div id="content">
+ @[if "error" in locals() and error != ""] <p>Fehler: <b>@error</b></p> @[end if]
+ @em_child_content
+</div>
+ \ No newline at end of file
diff --git a/py-bin/templates/logged_out.em b/py-bin/templates/logged_out.em
new file mode 100644
index 0000000..f096597
--- /dev/null
+++ b/py-bin/templates/logged_out.em
@@ -0,0 +1,5 @@
+@{em_inherit = "jman_setup_base.em"}
+@{title = "Ausgeloggt"}
+
+<p><b>Auf wiedersehen und viel Spass mit Jabber!</b></p>
+<p><a href="main.py">Neu einloggen</a></p>
diff --git a/py-bin/templates/login_fail.em b/py-bin/templates/login_fail.em
new file mode 100644
index 0000000..bc20d65
--- /dev/null
+++ b/py-bin/templates/login_fail.em
@@ -0,0 +1,22 @@
+@{em_inherit = "jman_base.em"}
+@{title = "Login fehlgeschlagen"}
+
+<div class="carbonbar">
+ <h2>@title</h2>
+</div>
+
+<div id="content">
+ <p>Grund: <b>@reason</b></p>
+ <p>Bitte logge dich mit den folgenden Angaben ein:</p>
+ <ul>
+ <li><b>E-Mail</b> Adresse (z.B. <i>alice@@immerda.ch</i>)</li>
+ <li><b>Jabber</b> Passwort</li>
+ </ul>
+ <p>Es kann auf den ersten Blick verwirren, dass du deine E-Mail
+ statt deiner Jabber Adresse angeben musst. Das ist aber nötig, da eine
+ Person mehrere Jabber Benutzerkonten haben kann. Somit brauchen wir deine
+ E-Mail Adresse, damit du nach dem Login alle deine Jabber Konten verwalten
+ kannst.
+ </p>
+ <p><a href="main.py">Nochmals versuchen</a></p>
+</div> \ No newline at end of file
diff --git a/py-bin/templates/login_form.em b/py-bin/templates/login_form.em
new file mode 100644
index 0000000..a90dcfb
--- /dev/null
+++ b/py-bin/templates/login_form.em
@@ -0,0 +1,26 @@
+@{em_inherit = "jman_base.em"}
+@{title = "Login"}
+
+<div class="carbonbar">
+ <h2>Registrierung</h2>
+</div>
+
+<div id="content">
+ <p>Bist du <b>neu hier</b>? Dann kannst du dich da
+ <a href="main.py?cmd=mail_form">registrieren</a>.
+ </p>
+</div>
+
+<div class="carbonbar">
+ <h2>Login</h2>
+</div>
+
+<div id="content">
+ <p>Hast du bereits ein Jabber Benutzerkonto? Dann logge dich hier ein.</p>
+ <form method="post" action="main.py?cmd=login_process">
+ <p>E-Mail: <input type="text" name="email"/></p>
+ <p>Jabber Passwort: <input type="password" name="jabberpw"/></p>
+ <p><input type="reset" value="Zurücksetzen"/>
+ <input type="submit" name="submitted" value="Login"/></p>
+ </form>
+</div> \ No newline at end of file
diff --git a/py-bin/templates/mail_error.em b/py-bin/templates/mail_error.em
new file mode 100644
index 0000000..01b70a9
--- /dev/null
+++ b/py-bin/templates/mail_error.em
@@ -0,0 +1,16 @@
+@{em_inherit = "jman_base.em"}
+@{title = "Fehler bei Registrierung"}
+
+<div class="carbonbar">
+ <h2>@title</h2>
+</div>
+
+<div id="content">
+ <p>Grund: <b>@reason</b></p>
+ <p>Um dich registrieren zu können, musst du eine <b>gültige E-Mail Adresse</b> angeben.
+ Beachte zudem, dass nur Domains erlaubt sind, welche <b>in Verbindung mit dem
+ Immerda Projekt</b> stehen. Wenn du glaubst, dass ein gewisser Domain zu Unrecht nicht
+ erlaubt ist, dann schreib ein E-Mail an
+ <a href="mailto:jabber AT immerda.ch"> jabber AT immerda.ch</a> (AT durch @@ ersetzen).
+ </p>
+</div> \ No newline at end of file
diff --git a/py-bin/templates/mail_form.em b/py-bin/templates/mail_form.em
new file mode 100644
index 0000000..604b8ab
--- /dev/null
+++ b/py-bin/templates/mail_form.em
@@ -0,0 +1,24 @@
+@{em_inherit = "jman_base.em"}
+@{title = "E-Mail Formular"}
+
+<div class="carbonbar">
+ <h2>@title</h2>
+</div>
+
+<div id="content">
+ <p>Zur Registration wird deine <b>E-Mail Adresse</b> benötigt. Damit nicht x-beliebige
+ Leute hier Jabber Konten registrieren, sind die Mail Domains beschränkt auf
+ <i>Immerda</i>, <i>Cronopios</i> und <i>Einfachsicher</i>. Erlaubte Adressen sind
+ also z.B.:
+ </p>
+ <ul>
+ <li><i>alice@@immerda.ch</i></li>
+ <li><i>bob@@cronopios.org</i></li>
+ <li><i>charlie@@einfachsicher.ch</i></li>
+ </ul>
+ <form method="post" action="main.py?cmd=mail_process">
+ <p>E-Mail: <input type="text" name="email"/></p>
+ <p><input type="reset" value="Zurücksetzen"/>
+ <input type="submit" name="submitted" value="Registrieren"/></p>
+ </form>
+</div>
diff --git a/py-bin/templates/mail_message.em b/py-bin/templates/mail_message.em
new file mode 100644
index 0000000..134a4a2
--- /dev/null
+++ b/py-bin/templates/mail_message.em
@@ -0,0 +1,21 @@
+From: Jabber Web Registration <@from_addr>
+To: @to_addr
+Date: @date
+Subject: Freischalt-Code
+
+Hallo und willkommen beim Jabber Netzwerk von Immerda!
+
+Du hast auf der Jabber Webseite des Immerda-Projekts ein Jabber
+Benutzerkonto registriert.
+
+Um das Benutzerkonto zu aktivieren und dein Jabber Passwort zu setzen,
+musst du dem unten angegebenen Link folgen. Alle weiteren Schritte
+sind auf dieser Seite beschrieben.
+
+@reg_link
+
+Falls du den Link nicht anklicken kannst, dann kopiere ihn einfach
+_vollständig_ (wichtig!) in die Adresszeile deines Browsers.
+
+Viel Spass wünscht dir
+Das Immerda Team \ No newline at end of file
diff --git a/py-bin/templates/mail_success.em b/py-bin/templates/mail_success.em
new file mode 100644
index 0000000..4efe73a
--- /dev/null
+++ b/py-bin/templates/mail_success.em
@@ -0,0 +1,15 @@
+@{em_inherit = "jman_base.em"}
+@{title = "E-Mail versandt"}
+
+<div class="carbonbar">
+ <h2>@title</h2>
+</div>
+
+<div id="content">
+ <p>Status: <b>@status</b></p>
+ <p>Ein Jabber Benutzerkonto ist nun für dich reserviert, doch du musst es erst noch
+ <b>aktivieren</b> und das <b>Passwort setzen</b>. In dem Mail, das in kürze bei dir
+ eintreffen wird, ist alles weitere beschrieben...
+ </p>
+ <p>Rufe nun deine Mailbox (<i>@email</i>) ab.</p>
+</div> \ No newline at end of file
diff --git a/py-bin/templates/mail_trylater.em b/py-bin/templates/mail_trylater.em
new file mode 100644
index 0000000..6f7421b
--- /dev/null
+++ b/py-bin/templates/mail_trylater.em
@@ -0,0 +1,16 @@
+@{em_inherit = "jman_base.em"}
+@{title = "Fehler bei Registrierung"}
+
+<div class="carbonbar">
+ <h2>@title</h2>
+</div>
+
+<div id="content">
+ <p>Grund: <b>@reason</b></p>
+ <p>
+ Vermutlich ist der Mail-Server momentan gerade überlastet. Versuche es also einfach
+ zu einem späteren Zeitpunkt <a href="main.py?cmd=mail_form">nochmals</a>.
+ Wenn der Fehler wiederholt auftritt, dann schreib ein E-Mail an
+ <a href="mailto:jabber AT immerda.ch"> jabber AT immerda.ch</a> (AT durch @@ ersetzen).
+ </p>
+</div> \ No newline at end of file
diff --git a/py-bin/templates/set_pw_form.em b/py-bin/templates/set_pw_form.em
new file mode 100644
index 0000000..7e7a3b7
--- /dev/null
+++ b/py-bin/templates/set_pw_form.em
@@ -0,0 +1,29 @@
+@{em_inherit = "jman_base.em"}
+@{title = "Passwort setzen"}
+
+@{if not "command" in locals(): command = "set_pw_process"}
+
+<div class="carbonbar">
+ <div id="userbar">
+ <i>@user_id</i>
+ @[if command == "set_pw_process"]
+ &nbsp;&nbsp;|&nbsp;
+ <a href="main.py?cmd=logout">Ausloggen</a>
+ @[end if]
+ </div>
+ <h2>@title</h2>
+</div>
+
+<div id="content">
+ @[if "error" in locals() and error != ""] <p>Fehler: <b>@error</b></p> @[end if]
+ <form method="post" action="main.py?cmd=@command">
+ <p>Passwort: <input type="password" name="password"/></p>
+ <p>Passwort bestätigen: <input type="password" name="password2"/></p>
+ <p><input type="reset" value="Zurücksetzen"/>
+ <input type="submit" name="submitted" value="Passwort setzen"/></p>
+ </form>
+
+ @[if command == "set_pw_process"]
+ <p><a href="main.py?cmd=setup_main">Zurück</a></p>
+ @[end if]
+</div>
diff --git a/py-bin/templates/setup_help.em b/py-bin/templates/setup_help.em
new file mode 100644
index 0000000..ac41f80
--- /dev/null
+++ b/py-bin/templates/setup_help.em
@@ -0,0 +1,75 @@
+@{em_inherit = "jman_base.em"}
+@{title = "Hilfe"}
+
+<div class="carbonbar">
+ <div id="userbar">
+ <i>@user_id</i>&nbsp;&nbsp;|&nbsp;
+ <a href="main.py?cmd=set_pw_form">Passwort ändern</a>&nbsp;&nbsp;|&nbsp;
+ <a href="main.py?cmd=logout">Ausloggen</a>
+ </div>
+ <h2>@title</h2>
+</div>
+
+<div id="content">
+ <p>Laut den Regeln des Immerda Projekts haben alle E-Mail NutzerInnen
+ von <i>Immerda</i>, <i>Cronopios</i> und <i>Einfachsicher</i>
+ ein Anrecht auf ein Jabber Benutzerkonto. Damit kein Chaos oder
+ gar Streitigkeiten bei der Namensvergabe enstehen, du jedoch trotzdem
+ eine Wahlmöglichkeit hast, werden zwei Optionen angeboten:
+ </p>
+</div>
+
+<div class="carbonbar">
+ <h3>Jabber Adressen <i>@@jabber.immerda.ch</i> usw.:</h3>
+</div>
+
+<div id="content">
+ <p>Diese Jabber Adressen werden nach einem fixen Schema <b>automatisch
+ aus den E-Mail adressen abgeleitet</b>. Die folgenden Beispiele zeigen,
+ wie das funktioniert:
+ </p>
+ <ul>
+ <li><i>alice@@immerda.ch</i> kriegt <i>alice@@<b>jabber</b>.immerda.ch</i></li>
+ <li><i>bob@@cronopios.org</i> kriegt <i>bob@@<b>jabber</b>.cronopios.org</i></li>
+ <li><i>charlie@@einfachsicher.ch</i> <i>kriegt charlie@@<b>jabber</b>.einfachsicher.ch</i></li>
+ </ul>
+ <p>Wenn du nun die Adresse <i>@jabber_id</i> verwendest, hast du folgende Vorteile:</p>
+ <ul>
+ <li>Leute, die deine E-Mail Adresse kennen, werden dich auch im Jabber finden.</li>
+ <li>Deine Gesprächspartner im Jabber können sicher sein, dass sie wirklich mit
+ <b>dir</b> reden, da nur <b>du als Besitzer der zugehörigen E-Mail Adresse</b>
+ diese Jabber Adresse haben kannst!
+ </li>
+ </ul>
+</div>
+
+<div class="carbonbar">
+ <h3>Jabber Adressen <i>@@imsg.ch</i>:</h3>
+</div>
+
+<div id="content">
+ <p>Während die Jabber Adressen <i>@@jabber.immerda.ch</i> etc. also das Vertrauen unter
+ Immerda-Leuten fördern können, kann es auch ein <b>Nachteil</b> sein, wenn deine Jabber
+ Adresse <b>in Verbindung mit deiner E-Mail</b> bei Immerda <b>gebracht</b> werden kann. Deshalb
+ hast du auch die Möglichkeit, eine Jabber Adresse <i>@@imsg.ch</i> auszuwählen,
+ solange diese noch frei ist. Hier gilt: "first come - first served".
+ </p>
+</div>
+
+<div class="carbonbar">
+ <h3>Fazit</h3>
+</div>
+
+<div id="content">
+ <p>Welche Variante für dich besser geeignet ist, kann sich <b>je nach Verwendung</b> von
+ Fall zu Fall ändern. Die beiden Varianten schliessen sich aber auch nicht gegenseitig
+ aus! Ausserdem kannst du auch späteren noch bei Bedarf auf dieser Seite zusätzliche
+ Jabber Adressen erstellen.
+ </p>
+ <p>Bei weiteren Fragen wende dich an
+ <a href="mailto:jabber AT immerda.ch"> jabber AT immerda.ch</a> (AT durch @@ ersetzen).
+ </p>
+ <a href="main.py?cmd=setup_main">Zurück</a>
+</div>
+
+
diff --git a/py-bin/templates/setup_main.em b/py-bin/templates/setup_main.em
new file mode 100644
index 0000000..3109f4b
--- /dev/null
+++ b/py-bin/templates/setup_main.em
@@ -0,0 +1,22 @@
+@{em_inherit = "jman_setup_base.em"}
+@{title = "Übersicht"}
+
+<p>Jabber Konten: [<a href="main.py?cmd=add_account_form">Hinzufügen</a>]</p>
+<ul>
+ <li><b><i>@jabber_id</i></b> (Standard)</li>
+ @[for jabber_id, url in account_list]
+ <li><i>@jabber_id</i> [<a href="@url">Löschen</a>]</li>
+ @[end for]
+</ul>
+
+@[if len(account_list) == 0]
+<p>
+ Im Moment ist dies deine einzige Jabber-Adresse.
+ Mit ihr kannst du Jabber uneingeschränkt benützen. Wenn dir diese Adresse jedoch
+ nicht gefällt, oder wenn du aus einem anderen Grund weitere Jabber Adressen
+ möchtest, so klicke oben auf <i>Hinzufügen</i>.
+</p>
+@[end if]
+<p>Eine ausführliche Erklärung zu den Adressen findest du in der
+ <a href="main.py?cmd=help">Hilfe</a>.
+</p> \ No newline at end of file
diff --git a/py-bin/utils.py b/py-bin/utils.py
new file mode 100644
index 0000000..b13949e
--- /dev/null
+++ b/py-bin/utils.py
@@ -0,0 +1,71 @@
+#helper functions/classes
+
+import string, logging, re, os, urllib
+import lib.jon.cgi as cgi, lib.jon.session as session
+import lib.em as em
+import config
+
+def xmap(function, data):
+ if isinstance(data, str):
+ return function(data)
+ if isinstance(data, int) or isinstance(data, long) or isinstance(data, float):
+ return function(str(data))
+ if isinstance(data, list):
+ return map(lambda x: xmap(function, x), data)
+ if isinstance(data, tuple):
+ return tuple(xmap(function, list(data)))
+
+def html_encode_struct(data):
+ return xmap(cgi.html_encode, data)
+
+def __empy_render_step(interpreter, filename, context_raw):
+ path = os.path.join(config.template_dir, filename)
+ out = interpreter.expand(file(path).read(), context_raw)
+ if "em_inherit" in context_raw:
+ filename = context_raw["em_inherit"]
+ del(context_raw["em_inherit"])
+ context_raw["em_child_content"] = out
+ out = __empy_render_step(interpreter, filename, context_raw)
+ return out
+
+def empy_render(filename, context={}, context_raw={}):
+ for k in context.keys():
+ context[k] = html_encode_struct(context[k])
+ context_raw.update(context)
+ interpreter = em.Interpreter()
+ out = __empy_render_step(interpreter, filename, context_raw)
+ interpreter.shutdown()
+ return out
+
+if config.debugmode:
+ HandlerBaseClass = cgi.DebugHandler
+else:
+ HandlerBaseClass = cgi.Handler
+class BasicHandler(HandlerBaseClass):
+ def process(self, req):
+ self.session = session.FileSession(req, config.the_secret, config.session_dir)
+ req.set_header("Content-Type", "text/html; charset=utf-8")
+ self.do_process(req)
+ self.session.save()
+
+ def render_template(self, req, filename, context={}, context_raw={}):
+ req.write(empy_render(filename, context, context_raw))
+
+ def error_page(self, req, message):
+ self.render_template(req, "error.em", dict(message=message))
+
+ def make_url(self, params):
+ return config.script_url + "?" + urllib.urlencode(params)
+
+ def redirect_to(self, req, url):
+ req.set_header("Status", "302 Temporarily moved")
+ req.set_header("Location", url)
+
+def process_request(handler):
+ cgi.CGIRequest(handler).process()
+
+def set_logging_defaults():
+ logging.basicConfig(level=logging.DEBUG,
+ format='%(asctime)s %(levelname)-8s %(message)s',
+ datefmt='%d.%m.%Y %H:%M:%S',
+ filename=config.logfile_path) \ No newline at end of file