From e9c3e953ed7e946a58c54c06e0dea49999176f3c Mon Sep 17 00:00:00 2001 From: banah Date: Thu, 15 Mar 2007 17:18:42 +0000 Subject: erster import mit neuer verzeichnisstruktur git-svn-id: https://rfd.cronopios.org/immerda/jabber@57 2b6ed08e-c90c-0410-9317-ae88bde05ade --- py-bin/config.py | 44 + py-bin/ejabberdctl.py | 47 + py-bin/jabberman.py | 392 ++++ py-bin/lib/__init__.py | 0 py-bin/lib/em.py | 3288 ++++++++++++++++++++++++++++++++ py-bin/lib/jon/__init__.py | 3 + py-bin/lib/jon/cgi.py | 673 +++++++ py-bin/lib/jon/session.py | 209 ++ py-bin/login.py | 24 + py-bin/mail_auth.py | 111 ++ py-bin/main.py | 31 + py-bin/setup.py | 140 ++ py-bin/templates/add_account_form.em | 17 + py-bin/templates/delete_account_ask.em | 8 + py-bin/templates/error.em | 13 + py-bin/templates/jman_base.em | 94 + py-bin/templates/jman_setup_base.em | 18 + py-bin/templates/logged_out.em | 5 + py-bin/templates/login_fail.em | 22 + py-bin/templates/login_form.em | 26 + py-bin/templates/mail_error.em | 16 + py-bin/templates/mail_form.em | 24 + py-bin/templates/mail_message.em | 21 + py-bin/templates/mail_success.em | 15 + py-bin/templates/mail_trylater.em | 16 + py-bin/templates/set_pw_form.em | 29 + py-bin/templates/setup_help.em | 75 + py-bin/templates/setup_main.em | 22 + py-bin/utils.py | 71 + 29 files changed, 5454 insertions(+) create mode 100644 py-bin/config.py create mode 100644 py-bin/ejabberdctl.py create mode 100644 py-bin/jabberman.py create mode 100644 py-bin/lib/__init__.py create mode 100755 py-bin/lib/em.py create mode 100644 py-bin/lib/jon/__init__.py create mode 100644 py-bin/lib/jon/cgi.py create mode 100644 py-bin/lib/jon/session.py create mode 100644 py-bin/login.py create mode 100644 py-bin/mail_auth.py create mode 100644 py-bin/main.py create mode 100644 py-bin/setup.py create mode 100644 py-bin/templates/add_account_form.em create mode 100644 py-bin/templates/delete_account_ask.em create mode 100644 py-bin/templates/error.em create mode 100644 py-bin/templates/jman_base.em create mode 100644 py-bin/templates/jman_setup_base.em create mode 100644 py-bin/templates/logged_out.em create mode 100644 py-bin/templates/login_fail.em create mode 100644 py-bin/templates/login_form.em create mode 100644 py-bin/templates/mail_error.em create mode 100644 py-bin/templates/mail_form.em create mode 100644 py-bin/templates/mail_message.em create mode 100644 py-bin/templates/mail_success.em create mode 100644 py-bin/templates/mail_trylater.em create mode 100644 py-bin/templates/set_pw_form.em create mode 100644 py-bin/templates/setup_help.em create mode 100644 py-bin/templates/setup_main.em create mode 100644 py-bin/utils.py (limited to 'py-bin') 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 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 ' +__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=", "Change prefix to something other than @"), +(" --no-prefix", "Do not do any markup processing at all"), +("-m --module=", "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=", "Specify file for output as write"), +("-a --append=", "Specify file for output as append"), +("-b --buffered-output", "Fully buffer output including open"), +(" --binary", "Treat the file as a binary"), +(" --chunk-size=", "Use this chunk size for reading binaries"), +("-P --preprocess=", "Interpret EmPy file before main processing"), +("-I --import=", "Import Python modules before processing"), +("-D --define=", "Execute Python assignment statement"), +("-E --execute=", "Execute Python statement before processing"), +("-F --execute-file=", "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=", "Set both input and output encodings"), +(" --unicode-input-encoding=", "Set input encoding"), +(" --unicode-output-encoding=", "Set output encoding"), +(" --unicode-errors=", "Set both input and output error handler"), +(" --unicode-input-errors=", "Set input error handler"), +(" --unicode-output-errors=", "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 "), +(PSEUDO_ENV, "Specify name of pseudomodule: -m "), +(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, '', 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, '')) + 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='', 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='', 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='', 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') + 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='', 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] [ [...]] +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 = '' % i + elif which == 'exec': + command = interpreter.string + target = '%s{%s}' % (_prefix, thing) + name = '' % i + elif which == 'file': + command = interpreter.string + name = '' % (i, thing) + target = '%s{execfile("""%s""")}' % (_prefix, thing) + elif which == 'import': + command = interpreter.string + name = '' % 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 = '' + 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 = { "&": "&", "<": "<", ">": ">", "\"": """, + "'": "'", "+": "+" } + +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(" ", " ") + + +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("" + "") + req.write("""\ + + + +jonpy traceback: %s + + + + + +
 
%s
%s
%s
+

A problem occurred in a Python script. Here is the sequence of +function calls leading up to the error, with the most recent first.

+""" % (_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("

%s: %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
    %s = %s" % + # (_tb_encode(name), _tb_encode(repr(getattr(evalue, name))))) + # req.error(" %s = %s\n" % (name, repr(getattr(evalue, name)))) + if html: + req.write("

\n") + frames = [] + records = inspect.getinnerframes(etb, 7) + records.reverse() + for frame, fn, lnum, func, lines, index in records: + if html: + req.write("""\ +""") + 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("" + "\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("\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("\n" % + (style, " " * (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("global%s = %s" % + (_tb_encode(name), _tb_encode(repr(value)))) + elif where == "builtin": + dump.append("builtin %s = %s" % (name, repr(value))) + htdump.append("builtin%s = %s" % + (_tb_encode(name), _tb_encode(repr(value)))) + elif where == "local": + dump.append("%s = %s" % (name, repr(value))) + htdump.append("%s = %s" % + (_tb_encode(name), _tb_encode(repr(value)))) + else: + dump.append("%s%s = %s" % (where, name.split(".")[-1], repr(value))) + htdump.append("%s%s = %s" % + (_tb_encode(where), _tb_encode(name.split(".")[-1]), + _tb_encode(repr(value)))) + else: + dump.append("%s undefined" % (name,)) + htdump.append("%s undefined" % (_tb_encode(name,))) + if html: + req.write("\n" % + (", ".join(htdump),)) + req.error(", ".join(dump) + "\n") + if html: + req.write("
%s in %s%s
%s
" + "%s %s
%s
\n") + if html: + req.write("\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("""\ +Error +

Error

+

Sorry, an error occurred. Please try again later.

+""") + + +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(" +

Name:

+

Domain: + +

+

+

+ + +

Zurück

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"} + +

Jabber Konto: @account

+ +

Abbrechen + | Löschen +

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"} + +
+

Unbehebbarer Fehler aufgetreten

+
+ +
+

Fehlermeldung: @message

+

Bitte wende dich an den technischen Support: + jabber AT immerda.ch (AT durch @@ ersetzen) +

+
\ 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 @@ + + + +Jabber Verwaltung - @title + + + +
+
+
+ +

Jabber Verwaltung

+ + @em_child_content + +
+ Copyleft (cc) 2007 + Immerda Projekt +
+
+
+
+ + \ 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"} + +
+
+ @[if "user_id" in locals()] + @user_id  |  + Passwort ändern  |  + Ausloggen + @[end if] +
+

@title

+
+ +
+ @[if "error" in locals() and error != ""]

Fehler: @error

@[end if] + @em_child_content +
+ \ 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"} + +

Auf wiedersehen und viel Spass mit Jabber!

+

Neu einloggen

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"} + +
+

@title

+
+ +
+

Grund: @reason

+

Bitte logge dich mit den folgenden Angaben ein:

+
    +
  • E-Mail Adresse (z.B. alice@@immerda.ch)
  • +
  • Jabber Passwort
  • +
+

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. +

+

Nochmals versuchen

+
\ 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"} + +
+

Registrierung

+
+ +
+

Bist du neu hier? Dann kannst du dich da + registrieren. +

+
+ +
+

Login

+
+ +
+

Hast du bereits ein Jabber Benutzerkonto? Dann logge dich hier ein.

+
+

E-Mail:

+

Jabber Passwort:

+

+

+
+
\ 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"} + +
+

@title

+
+ +
+

Grund: @reason

+

Um dich registrieren zu können, musst du eine gültige E-Mail Adresse angeben. + Beachte zudem, dass nur Domains erlaubt sind, welche in Verbindung mit dem + Immerda Projekt stehen. Wenn du glaubst, dass ein gewisser Domain zu Unrecht nicht + erlaubt ist, dann schreib ein E-Mail an + jabber AT immerda.ch (AT durch @@ ersetzen). +

+
\ 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"} + +
+

@title

+
+ +
+

Zur Registration wird deine E-Mail Adresse benötigt. Damit nicht x-beliebige + Leute hier Jabber Konten registrieren, sind die Mail Domains beschränkt auf + Immerda, Cronopios und Einfachsicher. Erlaubte Adressen sind + also z.B.: +

+
    +
  • alice@@immerda.ch
  • +
  • bob@@cronopios.org
  • +
  • charlie@@einfachsicher.ch
  • +
+
+

E-Mail:

+

+

+
+
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"} + +
+

@title

+
+ +
+

Status: @status

+

Ein Jabber Benutzerkonto ist nun für dich reserviert, doch du musst es erst noch + aktivieren und das Passwort setzen. In dem Mail, das in kürze bei dir + eintreffen wird, ist alles weitere beschrieben... +

+

Rufe nun deine Mailbox (@email) ab.

+
\ 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"} + +
+

@title

+
+ +
+

Grund: @reason

+

+ Vermutlich ist der Mail-Server momentan gerade überlastet. Versuche es also einfach + zu einem späteren Zeitpunkt nochmals. + Wenn der Fehler wiederholt auftritt, dann schreib ein E-Mail an + jabber AT immerda.ch (AT durch @@ ersetzen). +

+
\ 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"} + +
+
+ @user_id + @[if command == "set_pw_process"] +   |  + Ausloggen + @[end if] +
+

@title

+
+ +
+ @[if "error" in locals() and error != ""]

Fehler: @error

@[end if] +
+

Passwort:

+

Passwort bestätigen:

+

+

+
+ + @[if command == "set_pw_process"] +

Zurück

+ @[end if] +
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"} + +
+
+ @user_id  |  + Passwort ändern  |  + Ausloggen +
+

@title

+
+ +
+

Laut den Regeln des Immerda Projekts haben alle E-Mail NutzerInnen + von Immerda, Cronopios und Einfachsicher + 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: +

+
+ +
+

Jabber Adressen @@jabber.immerda.ch usw.:

+
+ +
+

Diese Jabber Adressen werden nach einem fixen Schema automatisch + aus den E-Mail adressen abgeleitet. Die folgenden Beispiele zeigen, + wie das funktioniert: +

+
    +
  • alice@@immerda.ch kriegt alice@@jabber.immerda.ch
  • +
  • bob@@cronopios.org kriegt bob@@jabber.cronopios.org
  • +
  • charlie@@einfachsicher.ch kriegt charlie@@jabber.einfachsicher.ch
  • +
+

Wenn du nun die Adresse @jabber_id verwendest, hast du folgende Vorteile:

+
    +
  • Leute, die deine E-Mail Adresse kennen, werden dich auch im Jabber finden.
  • +
  • Deine Gesprächspartner im Jabber können sicher sein, dass sie wirklich mit + dir reden, da nur du als Besitzer der zugehörigen E-Mail Adresse + diese Jabber Adresse haben kannst! +
  • +
+
+ +
+

Jabber Adressen @@imsg.ch:

+
+ +
+

Während die Jabber Adressen @@jabber.immerda.ch etc. also das Vertrauen unter + Immerda-Leuten fördern können, kann es auch ein Nachteil sein, wenn deine Jabber + Adresse in Verbindung mit deiner E-Mail bei Immerda gebracht werden kann. Deshalb + hast du auch die Möglichkeit, eine Jabber Adresse @@imsg.ch auszuwählen, + solange diese noch frei ist. Hier gilt: "first come - first served". +

+
+ +
+

Fazit

+
+ +
+

Welche Variante für dich besser geeignet ist, kann sich je nach Verwendung 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. +

+

Bei weiteren Fragen wende dich an + jabber AT immerda.ch (AT durch @@ ersetzen). +

+ Zurück +
+ + 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"} + +

Jabber Konten: [Hinzufügen]

+
    +
  • @jabber_id (Standard)
  • + @[for jabber_id, url in account_list] +
  • @jabber_id [Löschen]
  • + @[end for] +
+ +@[if len(account_list) == 0] +

+ 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 Hinzufügen. +

+@[end if] +

Eine ausführliche Erklärung zu den Adressen findest du in der + Hilfe. +

\ 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 -- cgit