Quixote-2.7b2/0000775000175000017500000000000011326377244011403 5ustar nasnasQuixote-2.7b2/doc/0000775000175000017500000000000011326377244012150 5ustar nasnasQuixote-2.7b2/doc/CHANGES_24.txt0000664000175000017500000012365411103004231014251 0ustar nasnas2.4 (2005-12-14) r27719: * Fix cimport bug (caused None to be bound to an imported name that did not exist). * Add Publisher.process(), which is like process_request() except that the caller provides an input stream and an environment instead of an HTTPRequest instance. Modify the included drivers to use process() instead of process_request(). * Update widgets.txt doc file (from Titus Brown). 2.3 (2005-10-18) r27556: * Remove some typechecking asserts from sendmail module to allow for unicode. * Make the generated http response header for content length be a str instead of an int. 2.2 (2005-9-27) r27459: * Remove check_session_addr from Config.config_var_list. * Remove Config.dump(). * When the PATH_INFO does not start with a '/' (or is empty), return a redirect instead of raising AssertionError. * Change htmltextObject_Check() to allow subtypes. * Improve unicode/charset handling. Add quixote.DEFAULT_CHARSET and use this as needed to encode log output, email bodies, and response bodies. When the response body is a str, let it pass through without re-encoding. When the response charset is None, omit the charset parameter from the content-type header. Use the default charset for decoding multipart requests. * Replace relative import in ptl_import.py. * Introduce a unicode-wrapper helper class to be used by htmltext formatting. It's purpose is to work around a PyString_Format bug that exists in all current versions of Python. Thanks go to Alexander J. Kozlovsky for the idea and for various other minor fixes. 2.1 (2005-8-9) r27151: * Make htmltag work better with unicode attribute values. * Modify publisher's exception handler to let SystemExit through. * Modify simple server so that SystemExit exceptions that make it to the server's error handler really are terminal. * Make encoding declarations work in ptl files. * Add HEAD request support to the simple server. * Modify simple server to avoid duplicate Date headers. * Add include_body keyword to response write(). 2.0 (2005-4-12) r26537: * Remove SessionError: just make a new session when the cookie does not identify an existing one. Remove CHECK_SESSION_ADDR configuration variable. * Gather ptl-related code into a sub-package: quixote.ptl. Add quixote.ptl.install: importing this module installs the ptl import hooks. Add quixote.ptl.ptlrun.py, a script for running ptl files from the command line. Gather the quixote.html and support files for htmltext into a sub-package. These changes should be compatible with current code, except that quixote.ptl_import is now in quixote.ptl.ptl_import, quixote.qx_distutils is now in quixote.ptl.qx_distutils, quixote._py_htmltext is now quixote.html._py_htmltext, and quixote._c_htmltext is now quixote.html._c_htmltext. * Insert REMOTE_USER environment value. * Change name: html_url() -> url_with_query() * Have StaticFile set the Expires header for 304 responses. * Add --https option to simple server to support https through stunnel. * Allow the label for the 'Add row' button to be specified for WidgetDict. * Add Session.get_user(). * Add Daniele Varrazzo's patch to _c_htmltext.c that makes it work on win32 with the mingw compiler. * Use private_msg when Directory raises TraversalError. * Allow setting of request's DEFAULT_CHARSET. 2.0a5 (2005-3-7) r26297: * SessionPublisher merged into Publisher. * Publisher 'session_mgr' keyword changed to 'session_manager'. * Form 'action_url' keyword and attribute changed to 'action.' * Make sure that HTTPResponse charset attribute is a str. * Don't require the traversed-into-object to be a Directory. * Make redirect() call str() on the location, so that calls from ptl templates don't need to. * Add an html_url() function that assembles a quoted url with a query. * Don't remove double-slashes from the path. * Make it easier to customize form error messages. * Make Form and Widget new-style classes so that properties can be used on them. * Call maintain_session on "interrupted" requests, too. * Add class variables with default values for content_type and charset. * Add HTTPRequest.get_fields(). * Add HTTPRequest.get_cookies(). * Remove name keyword from Form constructor. 2.0a4 (2005-1-19) r25893: * Remove support for $-substition. If you changed existing templates to work with 2.0a2 or 2.0a3 by escaping $'s, then you should change them back. * Change simple_server so that the PATH_INFO value is unquoted. * Add HTTPResponse.get_content_type(). * Update mod_python_handler.py to Quixote 2. 2.0a3 (2004-12-09) r25738: * Update most of the doc files. Add q1 -> q2 upgrade notes. Put notes at the start of the remaining doc files so that the reader knows that they have not been updated. * Make the simple_server provide remaining HTTP_* headers. * Generate a valid HTML document when listing static directories. Also, use get_path() instead of REQUEST_URI. * Work file upload into the basic forms demo. * Remove old session demo. The altdemo.py serves this purpose. * Add mini_demo. * Use the Twisted addCookie() method. It allows multiple Set-Cookie headers to end up in the response. * Fix a string formatting but that affected the htmltext type. 2.0a2 (2004-11-23) r25669: * Refactor the Publisher object. The new design gives "namespaces" more control over traversal. Packages are no longer imported automatically. By default, namespaces must be 'Directory' instances. The _q_access hook is gone. The 'namespace_stack' attribute is also gone. * Provide a Publisher object that works more like Quixote 1 (available in the 'publish1' module). Also, restore the original form framework with the name 'form1'. * Stop passing HTTPRequest to exported functions and _q_lookup(). You will need to use get_request() to access it. Also, require that _q_index be explicitly exported. * Remove the HTTPRequest.redirect() method. * Split error log and access log functionality into a separate class. Try to simplify site configuration. Configuration options can now be passed as keyword args to the Publisher class. The setup_logs() and shutdown_logs() methods are gone. Logs are now opened when the publisher is initialized. * Support $-substitution in templates, as in Python 2.4's string.Template class. $-substitution is applied to every '$'-containing literal in a template. Templates written before this change, if they contain literals containing '$', *must* be converted by replacing each '$' with '$$'. * Remove support for old style templates and support for versions of Python older than 2.3. * Use a common pattern for interfacing with HTTP servers. Simplify and cleanup the Medusa and Twisted code. Add a SCGI server module (it makes more sense to be part of Quixote rather than in the "scgi" package). * Rename HTTPRequest.get_form_var() to get_field(). That matches the terminology used by RFCs. * Add HTTPResponse.set_expires() method. * Add HTTPRequest.get_query() method. Use it instead of the QUERY_STRING environment variable. Add HTTPRequest.get_status_code(). * Compile .ptl files to .pyc files. Also, remove 'htmltext' and 'TemplateIO' from the globals of .ptl modules. * Move page compression functionality to http_response module. * Add a 'content_type' attribute to HTTPResponse. Refactor response to make it harder to screw up the character set of the response. * Add quixote.util.get_directory_path() for obtaining the list of _q_traversed() Directories (essentially a replacement for 'namespace_stack') * Move missing trailing slash detection and action to Directory.__call__() and remove FIX_TRAILING_SLASH configuration option. * Remove RUN_ONCE configuration variable and 'exit_now' attribute of publisher. * Stop generating 'Status' header for servers that don't need it. Add 'include_status' keyword to HTTPResponse.write(). * Overhaul the demo. 2.0a1 (2004-10-14) r25357: * Remove form1 library and rename form2 to form. * Remove HTTPRequest.dump_html() method and add dump_request to 'util' module. Remove obsolete functions from 'html' module (html_quote(), value_quote(), link() and render_tag()). * Use bool objects where appropriate. Prefer using builtin types over using the 'types' module. * Add support for unicode. The HTTPResponse object now has a set_charset() method. Also, the htmltext and TemplateIO types can contain unicode strings. * Add support to HTTPRequest for multipart/form-data type requests. Enhance MIME parsing so that it doesn't read huge lines into memory. Also implement parsing of application/x-www-form-urlencoded data rather than using the standard cgi.py module. Remove UPLOAD_DIR and UPLOAD_DIR_MODE config options. Uploaded files are now created using the tempfile module. * Remove the DEBUG_LOG configuration option. Debugging output now always goes to the error log. * Rewrite http_request.parse_cookie() and rename it to parse_cookies(). When generating cookies, escape quote characters that appear in cookie values. 1.2 (2004-10-06) r25277: * Fix Medusa server bug introduced in 1.1. HTTP headers must have "HTTP_" prepended to them. 1.1 (2004-10-05) r25259: * Fix example Apache directive for SCGI demo. * On Windows, put both stdin and stdout in binary mode. * Add 'buffered' flag to HTTPResponse (defaulting to True). * Support for the Python 2.4 'compiler' package. * Add HTTPRequest.get_scheme() method. * Use os.urandom() if it is available. Move randbytes() function to quixote.util module. * Convert tests to Sancho's utest framework. * Add 'index_filenames' keyword option to StaticDirectory. Medusa server: * Unquote the PATH_INFO environment variable. * Simplify propagation of HTTP headers. Form2 library: * Don't parse empty POSTs. * Fix a subtle bug in the multiple select widget. If nothing is selected then the value should be None, not [None]. * Add Widget.set_title(). 1.0 (2004-07-01) r24581: * No changes from 1.0c1. 1.0c1 (2004-06-15) r24468: * Fix some bugs in form2 library and improve the default look. * Add a 'content_type' attribute to Upload instances. 1.0b2 (2004-05-20) r24301: * Pass along more request headers when using twisted_http.py. * Add filter_output() hook (as suggested by Thomas Guettler). * Use plain text for the content of redirections. It's extremely unlikely that they will be seen anyhow. Use a modern DOCTYPE declaration for other pages that Quixote itself generates. * Fix code in upload.py that handles filename collisions. * Use a twisted producer for twisted_server.py (allows Stream responses to be efficient). Use twisted reactor instead of Application. Thanks to Jason Sibre. Form2 library changes: * Change how form2 widgets render themselves. The new approach makes it easier to build composite widgets and also provides more freedom to control the layout using CSS. Unfortunately, the forms will not look nice unless some CSS rules are provided. * Add recursive .has_error() and .clear_error() to widgets. * Ensure that has_errors() completely parse all the widgets. * Fix .clear_errors() for form2 Form. Errors can only be reliably cleared after the widget has parsed * Fix OptionSelect to always return a value from the options list. * Fix some bugs in WidgetList. * Check form token validity if tokens are enabled. 1.0b1 (2004-04-12) r23961: * Remove an underscore from the Session attributes __remote_address, __creation_time, and __access_time. Note that if you have pickled Session objects you will need to upgrade them somehow. * Use a simpler version of ptl_compile.parse() if using Python >= 2.3. Unfortunately, the hacks are still needed for older versions. * Change the behavior of session cookies if SESSION_COOKIE_PATH is unset. The old behavior was to not set the 'path' attribute of the session cookie. The new behavior is to use SCRIPT_NAME. Also, set the 'path' attribute when revoking the session cookie. Some browsers treat it as a separate cookie if it has a different path. * Don't try to compress stream responses. * Refactor PublishError exception handling. Requests will now be logged even if they raise an exception. This also allows some simplification to the Medusa and Twisted server code. * Make HTTPRequest.get_server() return HTTP_HOST if it is available rather than using SERVER_NAME and SERVER_PORT. * If "Host" header is present, use it to set SERVER_NAME and SERVER_PORT. * Make HTTPRequest.get_url() escape unsafe characters in the path part of the URL. * Use a simple counter instead of using the 'random' module when trying to generate unique names for uploaded files. * Allow arbitrary mapping objects to be used as the right hand operand when formating htmltext objects (i.e. as an argument to the htmltext.__mod__ method). The _c_htmltext implemention used to only allow 'dict' instances. Change _c_htmltext to allow any mapping. Also, instead of accessing every item in the mapping, use PyObject_GetItem() (aka __getitem__) to retrieve the values as they are required. That matches the behavior of PyString_Format(). * Don't set DEBUG_LOG in demo.conf. Extra logs files are confusing when people are starting out. * Significant form2 package changes. Remove FormComponent and related machinery. Allow widgets to control how they are rendered within forms (by overriding the form_render() method. Reorganize rendering code in the form module so that it is easier to subclass form and provide a different look. Change the parsing behavior of widgets. They no longer parse themselves when __init__ is called. Instead, each widget has a parse() method that calls _parse(), if it hasn't been called already, and returns the widgets value. Allow Widget._parse() to raise WidgetValueError in order to signal a parsing error. Add a CompositeWidget class. Remove Widget.get_value() since parse() seems to be sufficient. * Add HTTPS header when running under twisted_http.py. * Add cimport as a standard Quixote extension. * Disable FIX_TRAILING_SLASH in demo.conf. It can be confusing when trying to get the demo running. * Allow _q_exports to contain mappings of external names to internal names. * Twisted does not unquote the path so make twisted_http.py do it. * Implement error logging for mod_python handler. 0.7a3 (2003-12-03) r23260: * Improve init for ListWidget. The value of added_elements_widget needs to be set in the request if the list widget was added to the form after the first form submission. * Rename Form.WIDGET_ROW_CLASS to COMPONENT_CLASS, since that's what it really is. Make it an instance attribute as well as a class attribute, so it can be overridden at form construction time. * Remove 'allowed_values' and 'descriptions' from RadiobuttonsWidget.__init__. * Make htmltag() more efficient by avoiding repeated string concatenation. * Establish a pattern for way to specify html attributes for form2 widget constructors. The attributes may be provided in a dictionary using the 'attr' keyword. An html attribute 'foo' may also be specified in the widget constructor using using keyword 'foo' or 'foo_'. 0.7a2 (2003-11-11) r23130: * Implement publish_fcgi(). Don't use fcgi module for publish_cgi() since it's not completely portable. If you want to use FastCGI, you need to call publish_fcgi() instead of publish_cgi(). Don't import fcgi unless it's needed (since it does stuff at import time). * Allow StaticFile class to be overridden. Also, make it easier to subclass StaticDirectory. * When loading a PTL module, check if it exists in sys.modules. If it does, exec the code in that namespace rather than creating a new module. This is necessary to make reload() work. * Move HTTPUploadRequest import out of .create_request(). * Make HTTPRequest.guess_browser_version() more robust. * Remove 'allowed_values' and 'description' parameters from form2 SelectWidget. * Remove unused 'request' parameter from render() method of form2 RadiobuttonsWidget and OptionSelectWidget. * Improve form2 WidgetRow docstring. Don't use apply(). * Fix 'required' check in form2 WidgetRow. * Set the nb_inplace_add slot of TemplateIO types instead of sq_inplace_concat. nb_inplace_add takes precedence over __radd__ on instance types while sq_inplace_concat does not. * Fix typo in repr of _c_htmltext TemplateIO. * Use the quixote.sendmail module to send traceback email rather than calling /usr/sbin/sendmail. * Allow negative values to be passed to HTTPRequest.get_path(). * Add WidgetRow to form2.__init__. * Implement add_button() and add_reset() methods for form2.Form and rename Form._render_submit_buttons() to _render_button_widgets(). 0.7a1 (2003-10-09) r22720: * By default, don't build _c_htmltext on Win32. * In medusa_http.py, propagate 'GATEWAY_INTERFACE', 'SCRIPT_FILENAME', and HTTP headers. * Add HTTPRequest.get_method(). * Add support for keyword attributes on string widget. * Add a CollapsibleListWidget. * Allow HTTPResponse.body to be a Stream object as well as a string. Stream objects are useful if the body of the response is very large. * Change StaticFile to return Stream objects. Also, fix a minor bug in StaticDirectory (_q_lookup should return a namespace). * Improve installation instructions. * Add 'Redirector' helper class. * In publish.py, call _q_lookup(request, "") if _q_index does not exist and _q_lookup does exist. * In medusa_http.py, strip fragments from URLs (some versions of MSIE incorrectly send them at certain times; for example, a redirect includes them). Also, make REQUEST_URI contain the URI, not just the path. * In ptl_import, explicitly close the .ptlc file. This fixes a bug where a subsequent import may go for this .ptlc file and find it incomplete. * Add css_class keyword to htmltag so that class attributes can be specified easily. * Implement next generation form framework (currently called 'form2'). 0.6.1 (2003-07-14) r22004: * Make Form.add_widget() return the widget. * Allow the "Expires" header to be suppressed by setting the 'cache' attribute of HTTPResponse to None. * Use title case for header names added by HTTPResponse.set_headers(). Clients are not supposed to care about case but it's better to be conservative. * Catch IOError exceptions raised when writing the response and log a message (rather than exiting with a traceback). * Fix bug regarding _q_exception_handler. namespace_stack needs to be updated while the traversal is occuring. Thanks to Jason Sibre for spotting it and for the fix. * Add If-Modified-Since support for StaticFile objects. Also, don't set the Expires header by default. Instead, set the Last-Modified header based on the modification time of the file. The Expires header can be enabled by providing a value for the 'cache_time' argument. 0.6 final (2003-04-30) r21480: * Add a 'pass' to setup.py to make it easier to comment out the C extension. * Simplify 'From:' header in traceback e-mails. 0.6b6 (2003-04-15): * Rename _q_getname() to _q_lookup(). The name '_q_getname' is still supported, but will log a warning whenever it's encountered. This change will require users to modify their applications. * quixote.form.form has been translated from PTL to Python, meaning that you can now use the form framework without enabling PTL. (Original suggestion by Jim Dukarm, who also provided a patch that underwent considerable tweaking.) * Fix generation of temporary filenames in upload.py: filename collisions should be impossible now. * In medusa_http.py, convert HTTP headers into title case before passing them to Medusa, fixing duplicate Content-length headers. (Fix by Graham Fawcett.) * Added quixote.server.twisted_http, which serves a Quixote application using the Twisted event-driven framework (www.twistedmatrix.com). Contributed by Graham Fawcett. We don't use this code ourselves, but patches and bug fixes from Twisted users will be gratefully accepted. 0.6b5 (2003-03-13): * Fix incorrect parameter name for _traverse_url() 0.6b4 (2003-03-13): * The C version of the htmltext type is now compiled by default; you can edit setup.py manually to disable it. * StaticDirectory's list_folder argument renamed to list_directory. * StaticFile now supports the HTTP encoding for files. (Absence of this feature noted by Jim Dukarm.) * If Quixote looks for _q_index() in a namespace and doesn't find it, it raises AccessError (resulting in an HTTP 403 Forbidden error) rather than failing with an ImportError. A minor side effect of this change: Quixote will never attempt to import a module named '_q_index', nor will it pass '_q_index' to any _q_resolve() function. We don't expect this to be a backward compatibility problem . * Factored out the traverse_url() and get_component() method from the Publisher class. * Documented _q_exception_handler(). 0.6b3 (2003-03-07): * Fixed errors in demo_scgi.py. (David M. Cooke) * Avoid using True/False and enable nested scopes in _py_htmltext.py for Python 2.1 compatibility. (Noted by Jeff Bauer) Note that this means HTML templates will not work with Python 2.0 unless you compile the C extension. * Added StaticFile and StaticDirectory classes to quixote.util. Consult doc/static-files.txt for examples. (Contributed and documented by Hamish Lawson.) 0.6b2 (2003-01-27): * Added a new hook, _q_resolve(), that can be used to delay importing modules until they're actually accessed. Consult doc/programming.txt for an explanation. (Original suggestion and patch by Jon Corbet. In the process of adding it, Publisher.get_component() was rearranged to clarify the logic.) * Fixed the Medusa HTTP server to work with HTML templates (David M. Cooke) and to call finish_failed_request (pointed out by Graham Fawcett). * Added HTTP_USER_AGENT and ACCEPT_ENCODING to Medusa HTTP server. (Graham Fawcett) * Fixed _c_htmltext.c to compile on Windows. (Graham Fawcett) * Fixed two bugs in _c_htmltext.c found by code review, and one bug in _py_htmltext.py found by Nicola Larosa. (Neil Schemenauer) * Added a page to the demo that dumps the contents of the HTTPRequest. * Made upload.py write out HTTP upload data in binary mode, so binary content-types work correctly on Windows. (Graham Fawcett) * Added classifiers for use with Python 2.3's "register" command. 0.6b1 (2003-01-09): * Merged form/form.py and form/form_templates.ptl into form/form.ptl. (This means that you should completely remove (or rename) your old Quixote installation directory *before* installing 0.6, or the old form/form.py will shadow the new form.ptl.) * A new and preferred syntax for declaring PTL templates has been added. Instead of 'template func(): ...', the new form is 'def func [plain] ()'. This uses a notation that's been suggested for adding type information to Python functions. The Emacs Python mode already handles this properly, and it may be more compatible with future versions of Python. The 'template' keyword is still supported, but we encourage you to switch to the new syntax when you get a chance. * Quixote now supports a new kind of template that automatically performs HTML escaping. Here's an example. (Notice that the '[plain]' annotation is changed to '[html]' to enable this feature.) def header [html] (title): "%s" % title If the 'title' argument is something like "R&D", it will automatically be converted to "R&D" following the rules for escaping HTML special characters. The aim is to avoid cross-site scripting attacks by removing the need for the programmer to remember to HTML-escape unsafe text, instead relying on Quixote to escape text where necessary. See doc/PTL.txt for more information about how this works. This escaping is implemented using a 'htmltext' class implemented in Python, and is currently in production use on our web site. * An experimental C implementation of the 'htmltext' type is also included; it hasn't been put into production use yet. Edit setup.py and uncomment the appropriate line if you want to try the C implementation. * The form framework now uses automatic HTML escaping. This means that applications using the form framework will have to either be changed to use automatic HTML escaping themselves, or to use str() to convert 'htmltext' instances back to Python strings. See doc/upgrading.txt for more information. * Make Quixote a bit more friendly to multi-threaded applications by allowing multiple simultaneous requests (patch by Titus Brown). * Make util.xmlrpc() return an HTTP 405 Method Not Allowed error if the method isn't a POST. * Added demo/run_cgi.py, a script that makes it easy to write one file CGI applications that use Quixote. See the comments at the top of the demo/run_cgi.py file for instructions. 0.5.1 (2002-10-08): * (incompatible change for anyone doing HTTP upload with Quixote) Improved support for HTTP upload requests: any HTTP request with a Content-Type of "multipart/form-data" -- which is generally only used for uploads -- is now represented by HTTPUploadRequest, a subclass of HTTPRequest, and the uploaded files themselves are represented by Upload objects. See doc/upload.txt for details. * (possible incompatible changes for anyone subclassing Publisher, or using it for custom purposes) Various rearrangements and refactoring in the Publisher class. Added create_request() method, which takes responsibility for creating the HTTPRequest (or HTTPUploadRequest) object away from parse_request(). As a consequence, the signature of parse_request() has changed. Added process_request() method. Changed publish() so it catches exceptions coming from either parse_request() or process_request(). Consult the source code (publish.py) for details. * A new subpackage, quixote.server, is intended for code that publishes a Quixote application through HTTP, making it possible to run Quixote applications without having to configure Apache or some other full-blown Web server. Right now there's only an implementation on top of Medusa; contributions of support for Python's BaseHTTPServer, Twisted, or other frameworks would be welcome. * Modified SessionManager.maintain_session() so it explicitly removes a session if that session used to have useful info (ie. exists in the session manager), but no longer does (patch by Jon Corbet). * Make the PTL compiler a bit smarter about recognizing "template" lines; PTL code should now be able to use 'template' as an identifier, which is handy when converting existing Python code to PTL. * Replaced HTTPRequest.redirect() with a cleaner, more general version supplied by Andreas Kostyrka . Redirects to fully relative URLs (no leading slash) now work. * Added support for putting bits of JavaScript into HTML form documents: added HTTPResponse.add_javascript() to collect snippts of JavaScript code, and Form._render_javascript() to emit them as part of the HTML document. * Added global convenience functions get_path() and redirect(). * Change Publisher so it never modifies SCRIPT_NAME or PATH_INFO. * Fixed bug in quixote.sendmail._add_recip_headers(): it crashed if passed an empty list. * Factor out get_action_url() method in Form class. * Add the HTML version of the documentation to the source release. 0.5 (2002-06-10): * To fix installation problems on Win98 and Mac OS (pre OS X), setup.py now uses os.curdir instead of ''. * Overhauled handling of PublishError exceptions: Quixote now looks for the nearest _q_exception_handler() function in your application's namespace; the format_*error() methods of Publisher are gone. * Documented and overhauled the session management API. If you were previously using session management, you will almost certainly need to change your code; see doc/session-mgmt.txt and doc/session-upgrade.txt. If you've been wanting to use session management in your application but were put off by the lack of documentation, see doc/session-mgmt.txt. Specific changes: * removed the global singleton SessionManager object in session.py and several related functions * removed everything having to do with "application state", an unnecessary abstraction caused by premature over-generalization * removed the 'actual_user' attribute from Session -- it is specific to the MEMS Exchange and just confuses matters in Quixote * made most instance attributes of Session private * defined a sensible persistence API that should work with a wide variety of session persistence schemes * COOKIE_* config variables renamed to SESSION_COOKIE_* * Fix HTTPResponse so that the cookie domain and path can be None, and they will simply not be set in the cookie sent to the client -- that way the browser will simply do the right thing. Set COOKIE_DOMAIN and COOKIE_PATH in config.py to None, since that's usually fine. (You may need to set COOKIE_PATH to "/".) * Subtle but far-reaching change to the publishing algorithm: objects found by the publisher to handle the terminal component of a URL can now be strings as well as callables; a string simply substitutes for a callable's return value. The immediate reason for this was to allow _q_lookup() functions to return a string, but a consequence is that you can now put static text in global variables and simply publish them. * Add CHECK_SESSION_ADDR config variable to control whether we check that requests in a session all come from the same IP address, as a defence against playback attacks. (Thanks to Jonathan Corbet.) * In error reports, print the traceback first, ahead of form variables, cookies, and the environment. * Include the HTTP_USER_AGENT variable in access log lines. * Add 'sort' option to SelectWidget class, to force the list of allowed values to be sorted in case-insensitive lexicographic order, with None first. 0.4.7 (2002-04-18): * Move ACCESS_TIME_RESOLUTION to SessionManager class. This was another embarrassing bug introduced in 0.4.5. * In http_request.py, make the test that prevents stdin from being consumed less restrictive (e.g. for PUT methods). * Add some simple test code. 0.4.6 (2002-04-12): * a last-minute patch to http_request.py just before release 0.4.5 broke extracting form data from GET requests -- fixed that 0.4.5 (2002-04-11): * The meaning of the DISPLAY_EXCEPTIONS configuration variable has changed. It's no longer a Boolean, and instead can take three different values: None (or any false value) [default] an "Internal Server Error" page that exposes no information about the traceback 'plain' a plain text page showing the traceback and the request variables 'html' a more elaborate HTML display showing the local variables and a few lines of context for each level of the traceback. (This setting requires the cgitb module that comes with Python 2.2.) (Idea and first version of the patch by David Ascher) * Fixed SessionManager.expire_session() method so it actually works (spotted by Robin Wohler). * Fixed docs so they don't refer to the obsolete URL_PREFIX configuration variable (spotted by Robin Wohler). * Various other documentation tweaks and improvements. * Fixed sample Apache rewrite rules in demo.txt and web-server.txt (spotted by Joel Shprentz). * Generate new form tokens when rendering a form rather then when intializing it. This prevents an extra token from being created when processing a valid form (suggested by Robin Wohler). * Ensure filenames are included in SyntaxError tracebacks from PTL modules. * Changed format of session cookies: they're now just random 64-bit numbers in hex. * Use HTTP 1.1 cache control headers ("Date" and "Expires") instead of the older "Pragma: no-cache". * In the form/widget library: make some effort to generate HTML that is XHTML-compliant. * New method: HTTPRequest.get_accepted_types() returns the MIME content types a client will accept as a dictionary mapping MIME type to the quality factor. (Example: {'text/html':1.0, 'text/plain':0.5, ...}) * Changed escape hatch for XML-RPC handlers; standard input will only be consumed when the HTTP method is POST and the Content-Type is either application/x-www-form-urlencoded or multipart/form-data. * Added quixote.util module to contain various miscellaneous utility functions. Right now, it contains a single function for processing requests as XML-RPC invocations. 0.4.4 (2002-01-29): * Simplify munging of SCRIPT_NAME variable, fixing a bug. Depending on how Quixote was called, the path could have been appended to SCRIPT_NAME without a separating slash. (Found by Quinn Dunkan.) * On Windows, set mode of sys.stdout to binary. This is important because responses may contain binary data. Also, EOL translation can throw off content length calculations. (Found by David Ascher) * Added a demonstration of the form framework. (Neil) * Added an escape hatch for XML-RPC handlers; http_request.process_inputs() will no longer consume all of standard input when the Content-Type is text/xml. * Removed a debug print from form.widget. 0.4.3 (2001-12-17): * Removed the URL_PREFIX configuration variable; it's not actually needed anywhere, and caused some user confusion. * Added FORM_TOKENS configuration variable to enable/disable unique form identifiers. (These are useful as a measure against cross-site request forgery [CSRF] attacks, but disabled by default because some form of persistent session management is required, which is not currently included with Quixote.) * Added demonstration and documentation for the widget classes (the first part of the Quixote Form Library). * Added HTTPResponse.set_content_type() method. * Fixed some minor bugs in the widget library. * Fixed to work with Python 2.2. * Greatly reduced the set of symbols imported by "from quixote import *" -- it's useful for interactive sessions. 0.4.2 (2001-11-14): * Made the quixote.sendmail module a bit more flexible and robust. * Fix so it doesn't blow up under Windows if debug logging is disabled (ie. write to NUL, not /dev/null). * Clarified some documenation inconsistencies, and added description of logging to doc/programming.txt. * Fixed some places that we forgot to update when the PTL-related modules were renamed. * Fixed ptl_compile.py so PTL tracebacks include the full path of source file(s). * Fixed bug where a missing _q_index() triggered a confusing ImportError; now it triggers a TraversalError, as expected. * Various fixes and improvements to the Config class. * Miscellaneous fixes to session.py. * Miscellaneous fixes to widget classes. * Reorganized internal PTL methods of the Form class. * Removed the "test" directory from the distribution, since it's not used for anything -- ie., there's no formal test suite yet ;-( 0.4.1 (2001-10-10): * Made access logging a little more portable (don't depend on Apache's REQUEST_URI environment variable). * Work around the broken value of PATH_INFO returned by IIS. * Work around IIS weird handling of SERVER_SECURE_PORT (for non-SSL requests, it is set to "0"). * Reassign sys.stderr so all application output to stderr goes to the Quixote error log. 0.4 (2001-10-04): * TraversalError now takes a public and a private message, instead of just a single message string. The private message is shown if SECURE_ERRORS is false; otherwise, the public message is shown. See the class docstring for TraversalError for more details. * Add the Quixote Form Library, a basic form and widget framework for HTML. * Allow classes and functions inside PTL modules. * Return a string object from templates rather than a TemplateIO instance. * Improve the security of session cookies. * Don't save empty sessions. * Detect expired sessions. * Add the quixote.sendmail module, useful for applications that need to send outgoing mail (as many web apps do). * Code reorganization -- various modules moved or renamed: quixote.util.fcgi -> quixote.fcgi quixote.compile_template -> quixote.ptl_compile quixote.imphooks -> quixote.ptl_import quixote.dumpptlc -> quixote.ptcl_dump * More code reorganization: the quixote.zope package is gone, as are the BaseRequest and BaseResponse modules. Only HTTPRequest and HTTPResponse survive, in the quixote.http_request and quixote.http_response modules. All remaining Zope-isms have been removed, so the code now looks much like the rest of Quixote. Many internal interfaces changed. * Added the quixote.mod_python module, contributed by Erno Kuusela . Allows Quixote applications to be driven by the Apache module mod_python, so no CGI or or FastCGI driver script is required. 0.3 (2001-06-11): * Now supports Python 2.1. * Names of the form __*__ are reserved for Python, and 2.1 is beginning to enforce this rule. Accordingly the Quixote special methods have been renamed: __access__ -> _q_access __exports__ -> _q_exports __getname__ -> _q_getname index -> _q_index * Massive changes to quixote.publisher and quixote.config, to make the publishing loop more flexible and more easily changed by applications. For example, it's now possible to catch the ZODB's ConflictErrors and retry an operation. * Added an ACCESS_LOG configuration setting, which allows setting up a file logging every call made to Quixote. * The error log now contains the time of each error, and a dump of the user's session object. * Added handy functions for getting request, session, user, etc.: quixote.get_publisher(), quixote.get_request(), quixote.get_session(), quixote.get_user(). * quixote.publish can now gzip-compress its output if the browser claims to support it. Only the 'gzip' and 'x-gzip' content encodings are supported; 'deflate' isn't because we couldn't get it to work reliably. Compression can be enabled by setting the 'compress_pages' config option to true. * Some fixes and minor optimizations to the FCGI code. * Added HTTPRequest.get_encoding() method to find the encodings a client accepts. 0.2 (2001-01-16): * Only pass HTTPRequest object to published functions. The HTTPResponse object is available as an attribute of the request. * Removed more unused Zope code from HTTPRequest. Add redirect() method to HTTPRequest. * Simplify HTTPResponse. __init__() no longer requires the server name. redirect() requires a full URL. * Fix a bug in the PTL compiler. PTL modules can now have doc strings. * Added a config parser. Individual Quixote applications can now have their own configuration settings (overriding the Quixote defaults). See the config.py module for details. * Re-wrote the exception handling code for exceptions raised inside of published functions. * Non-empty PATH_INFO is no longer supported. __getname__ or query strings are a cleaner solution. * Add FIX_TRAILING_SLASH option and make code changes to carefully preserve trailing slashes (ie. an empty component on the end of paths). * Session management has been over-hauled. DummySessionManager can be used for applications that don't require sessions. * Set Content-length header correctly in HTTPResponse object * Added a demo application. 0.1: * Officially given a license (the Python 1.6 license, so it's free software). * Added SECURE_ERRORS variable to prevent exception tracebacks from being returned to the Web browser * Added a __getname__() function to traversal, which is called if the current name isn't in the current namespace's export list. This allows interpolation of arbitrary user object IDs into the URL, which is why it has to circumvent the __exports__ check: the object IDs won't be known until runtime, so it would be silly to add them to __exports__. Very useful and powerful feature, but it has security implications, so be careful! * compile_template.py should now work for both Python 1.5.2 or 2.0. * Better reporting of syntax errors in PTL * Always assume regular CGI on Windows 0.02 (2000-08-12): * Neil Schemenauer has completely rewritten the PTL compiler and changed the syntax to match Python's. The compiler now relies on Jeremy Hylton's compiler code from the Python 2.0 CVS tree. * Added functions to quixote.sessions: get_session(), has_session(), get_app_state() * Simplified reload-checking logic * Added .browser_version() method to HTTPRequest * Various bugfixes * session classes slightly tweaked, so you can subclass them * Added .session attribute to request object * Added quixote.errors module to hold exceptions 0.01: * Initial release. Quixote-2.7b2/doc/INSTALL.html0000664000175000017500000000330211103004231014112 0ustar nasnas

Installing Quixote

Quixote requires Python 2.3 or later.

If you have a previously installed quixote, we strongly recommend that you remove it before installing a new one. First, find out where your old Quixote installation is:

python -c "import os, quixote; print os.path.dirname(quixote.__file__)"

and then remove away the reported directory. (If the import fails, then you don't have an existing Quixote installation.)

Now install the new version by running (in the distribution directory),

python setup.py install

and you're done.

Quick start

In a terminal window, run server/simple_server.py. In a browser, open http://localhost:8080

Upgrading a Quixote 1 application to Quixote 2.

See upgrading.txt for details.

Quixote-2.7b2/doc/INSTALL.txt0000664000175000017500000000146011103004231013770 0ustar nasnasInstalling Quixote ================== Quixote requires Python 2.3 or later. If you have a previously installed quixote, we strongly recommend that you remove it before installing a new one. First, find out where your old Quixote installation is: python -c "import os, quixote; print os.path.dirname(quixote.__file__)" and then remove away the reported directory. (If the import fails, then you don't have an existing Quixote installation.) Now install the new version by running (in the distribution directory), python setup.py install and you're done. Quick start =========== In a terminal window, run server/simple_server.py. In a browser, open http://localhost:8080 Upgrading a Quixote 1 application to Quixote 2. =============================================== See upgrading.txt for details. Quixote-2.7b2/doc/LICENSE_24.txt0000664000175000017500000000652111103004231014254 0ustar nasnasCNRI OPEN SOURCE LICENSE AGREEMENT FOR QUIXOTE-2.4 IMPORTANT: PLEASE READ THE FOLLOWING AGREEMENT CAREFULLY. BY COPYING, INSTALLING OR OTHERWISE USING QUIXOTE-2.4 SOFTWARE, YOU ARE DEEMED TO HAVE AGREED TO BE BOUND BY THE TERMS AND CONDITIONS OF THIS LICENSE AGREEMENT. 1. This LICENSE AGREEMENT is between Corporation for National Research Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 ("CNRI"), and the Individual or Organization ("Licensee") copying, installing or otherwise using Quixote-2.4 software in source or binary form and its associated documentation ("Quixote-2.4"). 2. Subject to the terms and conditions of this License Agreement, CNRI hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Quixote-2.4 alone or in any derivative version, provided, however, that CNRI's License Agreement and CNRI's notice of copyright, i.e., "Copyright © 2005 Corporation for National Research Initiatives; All Rights Reserved" are retained in Quixote-2.4 alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates Quixote-2.4, or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Quixote-2.4. 4. CNRI is making Quixote-2.4 available to Licensee on an "AS IS" basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF QUIXOTE-2.4 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF QUIXOTE-2.4 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING QUIXOTE-2.4, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. This License Agreement shall be governed by the federal intellectual property law of the United States, including without limitation the federal copyright law, and, to the extent such U.S. federal law does not apply, by the law of the Commonwealth of Virginia, excluding Virginia's conflict of law provisions. Notwithstanding the foregoing, with regard to derivative works based on Quixote-2.4 that incorporate non-separable material that was previously distributed under the GNU General Public License (GPL), the law of the Commonwealth of Virginia shall govern this License Agreement only as to issues arising under or with respect to Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between CNRI and Licensee. This License Agreement does not grant permission to use CNRI trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Quixote-2.4, Licensee agrees to be bound by the terms and conditions of this License Agreement. Quixote-2.7b2/doc/Makefile0000664000175000017500000000105711114154100013566 0ustar nasnas# # Makefile to convert Quixote docs to HTML # TXT_FILES = $(wildcard *.txt) HTML_FILES = $(filter-out LICENSE_24% CHANGES_24%,$(TXT_FILES:%.txt=%.html)) RST2HTML = rst2html.py RST2HTML_OPTS = -o us-ascii DEST_HOST = staging.mems-exchange.org DEST_DIR = /www/www-docroot/software/quixote/doc SS = default.css %.html: %.txt $(RST2HTML) $(RST2HTML_OPTS) $< $@ all: $(HTML_FILES) clean: rm -f $(HTML_FILES) install: rsync -vptgo *.html $(SS) $(DEST_HOST):$(DEST_DIR) local-install: dir=`pwd` ; \ cd $(DEST_DIR) && ln -sf $$dir/*.html $$dir/$(SS) . Quixote-2.7b2/doc/PTL.html0000664000175000017500000003613411103004231013454 0ustar nasnas PTL: Python Template Language

PTL: Python Template Language

Introduction

PTL is the templating language used by Quixote. Most web templating languages embed a real programming language in HTML, but PTL inverts this model by merely tweaking Python to make it easier to generate HTML pages (or other forms of text). In other words, PTL is basically Python with a novel way to specify function return values.

Specifically, a PTL template is designated by inserting a [plain] or [html] modifier after the function name. The value of expressions inside templates are kept, not discarded. If the type is [html] then non-literal strings are passed through a function that escapes HTML special characters.

Plain text templates

Here's a sample plain text template:

def foo [plain] (x, y = 5):
    "This is a chunk of static text."
    greeting = "hello world" # statement, no PTL output
    print 'Input values:', x, y
    z = x + y
    """You can plug in variables like x (%s)
in a variety of ways.""" % x

    "\n\n"
    "Whitespace is important in generated text.\n"
    "z = "; z
    ", but y is "
    y
    "."

Obviously, templates can't have docstrings, but otherwise they follow Python's syntactic rules: indentation indicates scoping, single-quoted and triple-quoted strings can be used, the same rules for continuing lines apply, and so forth. PTL also follows all the expected semantics of normal Python code: so templates can have parameters, and the parameters can have default values, be treated as keyword arguments, etc.

The difference between a template and a regular Python function is that inside a template the result of expressions are saved as the return value of that template. Look at the first part of the example again:

def foo [plain] (x, y = 5):
    "This is a chunk of static text."
    greeting = "hello world" # statement, no PTL output
    print 'Input values:', x, y
    z = x + y
    """You can plug in variables like x (%s)
in a variety of ways.""" % x

Calling this template with foo(1, 2) results in the following string:

This is a chunk of static text.You can plug in variables like x (1)
in a variety of ways.

Normally when Python evaluates expressions inside functions, it just discards their values, but in a [plain] PTL template the value is converted to a string using str() and appended to the template's return value. There's a single exception to this rule: None is the only value that's ever ignored, adding nothing to the output. (If this weren't the case, calling methods or functions that return None would require assigning their value to a variable. You'd have to write dummy = list.sort() in PTL code, which would be strange and confusing.)

The initial string in a template isn't treated as a docstring, but is just incorporated in the generated output; therefore, templates can't have docstrings. No whitespace is ever automatically added to the output, resulting in ...text.You can ... from the example. You'd have to add an extra space to one of the string literals to correct this.

The assignment to the greeting local variable is a statement, not an expression, so it doesn't return a value and produces no output. The output from the print statement will be printed as usual, but won't go into the string generated by the template. Quixote directs standard output into Quixote's debugging log; if you're using PTL on its own, you should consider doing something similar. print should never be used to generate output returned to the browser, only for adding debugging traces to a template.

Inside templates, you can use all of Python's control-flow statements:

def numbers [plain] (n):
    for i in range(n):
        i
        " " # PTL does not add any whitespace

Calling numbers(5) will return the string "1 2 3 4 5 ". You can also have conditional logic or exception blocks:

def international_hello [plain] (language):
    if language == "english":
        "hello"
    elif language == "french":
        "bonjour"
    else:
        raise ValueError, "I don't speak %s" % language

HTML templates

Since PTL is usually used to generate HTML documents, an [html] template type has been provided to make generating HTML easier.

A common error when generating HTML is to grab data from the browser or from a database and incorporate the contents without escaping special characters such as '<' and '&'. This leads to a class of security bugs called "cross-site scripting" bugs, where a hostile user can insert arbitrary HTML in your site's output that can link to other sites or contain JavaScript code that does something nasty (say, popping up 10,000 browser windows).

Such bugs occur because it's easy to forget to HTML-escape a string, and forgetting it in just one location is enough to open a hole. PTL offers a solution to this problem by being able to escape strings automatically when generating HTML output, at the cost of slightly diminished performance (a few percent).

Here's how this feature works. PTL defines a class called htmltext that represents a string that's already been HTML-escaped and can be safely sent to the client. The function htmlescape(string) is used to escape data, and it always returns an htmltext instance. It does nothing if the argument is already htmltext.

If a template function is declared [html] instead of [text] then two things happen. First, all literal strings in the function become instances of htmltext instead of Python's str. Second, the values of expressions are passed through htmlescape() instead of str().

htmltext type is like the str type except that operations combining strings and htmltext instances will result in the string being passed through htmlescape(). For example:

>>> from quixote.html import htmltext
>>> htmltext('a') + 'b'
<htmltext 'ab'>
>>> 'a' + htmltext('b')
<htmltext 'ab'>
>>> htmltext('a%s') % 'b'
<htmltext 'ab'>
>>> response = 'green eggs & ham'
>>> htmltext('The response was: %s') % response
<htmltext 'The response was: green eggs &amp; ham'>

Note that calling str() strips the htmltext type and should be avoided since it usually results in characters being escaped more than once. While htmltext behaves much like a regular string, it is sometimes necessary to insert a str() inside a template in order to obtain a genuine string. For example, the re module requires genuine strings. We have found that explicit calls to str() can often be avoided by splitting some code out of the template into a helper function written in regular Python.

It is also recommended that the htmltext constructor be used as sparingly as possible. The reason is that when using the htmltext feature of PTL, explicit calls to htmltext become the most likely source of cross-site scripting holes. Calling htmltext is like saying "I am absolutely sure this piece of data cannot contain malicious HTML code injected by a user. Don't escape HTML special characters because I want them."

Note that literal strings in template functions declared with [html] are htmltext instances, and therefore won't be escaped. You'll only need to use htmltext when HTML markup comes from outside the template. For example, if you want to include a file containing HTML:

def output_file [html] ():
    '<html><body>' # does not get escaped
    htmltext(open("myfile.html").read())
    '</body></html>'

In the common case, templates won't be dealing with HTML markup from external sources, so you can write straightforward code. Consider this function to generate the contents of the HEAD element:

def meta_tags [html] (title, description):
    '<title>%s</title>' % title
    '<meta name="description" content="%s">\n' % description

There are no calls to htmlescape() at all, but string literals such as <title>%s</title> have all be turned into htmltext instances, so the string variables will be automatically escaped:

>>> t.meta_tags('Catalog', 'A catalog of our cool products')
<htmltext '<title>Catalog</title>
  <meta name="description" content="A catalog of our cool products">\n'>
>>> t.meta_tags('Dissertation on <HEAD>', 
...             'Discusses the "LINK" and "META" tags')
<htmltext '<title>Dissertation on &lt;HEAD&gt;</title>
  <meta name="description" 
   content="Discusses the &quot;LINK&quot; and &quot;META&quot; tags">\n'>
>>>

Note how the title and description have had HTML-escaping applied to them. (The output has been manually pretty-printed to be more readable.)

Once you start using htmltext in one of your templates, mixing plain and HTML templates is tricky because of htmltext's automatic escaping; plain templates that generate HTML tags will be double-escaped. One approach is to just use HTML templates throughout your application. Alternatively you can use str() to convert htmltext instances to regular Python strings; just be sure the resulting string isn't HTML-escaped again.

Two implementations of htmltext are provided, one written in pure Python and a second one implemented as a C extension. Both versions have seen production use.

PTL modules

PTL templates are kept in files with the extension .ptl. Like Python files, they are byte-compiled on import, and the byte-code is written to a compiled file with the extension .pyc. Since vanilla Python doesn't know anything about PTL, Quixote provides an import hook to let you import PTL files just like regular Python modules. The standard way to install this import hook is by calling the enable_ptl() function:

from quixote import enable_ptl
enable_ptl()

(Note: if you're using ZODB, always import ZODB before installing the PTL import hook. There's some interaction which causes importing the TimeStamp module to fail when the PTL import hook is installed; we haven't debugged the problem. A similar problem has been reported for BioPython and win32com.client imports.)

Once the import hook is installed, PTL files can be imported as if they were Python modules. If all the example templates shown here were put into a file named foo.ptl, you could then write Python code that did this:

from foo import numbers
def f():
    return numbers(10)

You may want to keep this little function in your PYTHONSTARTUP file:

def ptl():
    try:
        import ZODB
    except ImportError:
        pass
    from quixote import enable_ptl
    enable_ptl()

This is useful if you want to interactively play with a PTL module.

Quixote-2.7b2/doc/PTL.txt0000664000175000017500000002533011103004231013323 0ustar nasnasPTL: Python Template Language ============================= Introduction ------------ PTL is the templating language used by Quixote. Most web templating languages embed a real programming language in HTML, but PTL inverts this model by merely tweaking Python to make it easier to generate HTML pages (or other forms of text). In other words, PTL is basically Python with a novel way to specify function return values. Specifically, a PTL template is designated by inserting a ``[plain]`` or ``[html]`` modifier after the function name. The value of expressions inside templates are kept, not discarded. If the type is ``[html]`` then non-literal strings are passed through a function that escapes HTML special characters. Plain text templates -------------------- Here's a sample plain text template:: def foo [plain] (x, y = 5): "This is a chunk of static text." greeting = "hello world" # statement, no PTL output print 'Input values:', x, y z = x + y """You can plug in variables like x (%s) in a variety of ways.""" % x "\n\n" "Whitespace is important in generated text.\n" "z = "; z ", but y is " y "." Obviously, templates can't have docstrings, but otherwise they follow Python's syntactic rules: indentation indicates scoping, single-quoted and triple-quoted strings can be used, the same rules for continuing lines apply, and so forth. PTL also follows all the expected semantics of normal Python code: so templates can have parameters, and the parameters can have default values, be treated as keyword arguments, etc. The difference between a template and a regular Python function is that inside a template the result of expressions are saved as the return value of that template. Look at the first part of the example again:: def foo [plain] (x, y = 5): "This is a chunk of static text." greeting = "hello world" # statement, no PTL output print 'Input values:', x, y z = x + y """You can plug in variables like x (%s) in a variety of ways.""" % x Calling this template with ``foo(1, 2)`` results in the following string:: This is a chunk of static text.You can plug in variables like x (1) in a variety of ways. Normally when Python evaluates expressions inside functions, it just discards their values, but in a ``[plain]`` PTL template the value is converted to a string using ``str()`` and appended to the template's return value. There's a single exception to this rule: ``None`` is the only value that's ever ignored, adding nothing to the output. (If this weren't the case, calling methods or functions that return ``None`` would require assigning their value to a variable. You'd have to write ``dummy = list.sort()`` in PTL code, which would be strange and confusing.) The initial string in a template isn't treated as a docstring, but is just incorporated in the generated output; therefore, templates can't have docstrings. No whitespace is ever automatically added to the output, resulting in ``...text.You can ...`` from the example. You'd have to add an extra space to one of the string literals to correct this. The assignment to the ``greeting`` local variable is a statement, not an expression, so it doesn't return a value and produces no output. The output from the ``print`` statement will be printed as usual, but won't go into the string generated by the template. Quixote directs standard output into Quixote's debugging log; if you're using PTL on its own, you should consider doing something similar. ``print`` should never be used to generate output returned to the browser, only for adding debugging traces to a template. Inside templates, you can use all of Python's control-flow statements:: def numbers [plain] (n): for i in range(n): i " " # PTL does not add any whitespace Calling ``numbers(5)`` will return the string ``"1 2 3 4 5 "``. You can also have conditional logic or exception blocks:: def international_hello [plain] (language): if language == "english": "hello" elif language == "french": "bonjour" else: raise ValueError, "I don't speak %s" % language HTML templates -------------- Since PTL is usually used to generate HTML documents, an ``[html]`` template type has been provided to make generating HTML easier. A common error when generating HTML is to grab data from the browser or from a database and incorporate the contents without escaping special characters such as '<' and '&'. This leads to a class of security bugs called "cross-site scripting" bugs, where a hostile user can insert arbitrary HTML in your site's output that can link to other sites or contain JavaScript code that does something nasty (say, popping up 10,000 browser windows). Such bugs occur because it's easy to forget to HTML-escape a string, and forgetting it in just one location is enough to open a hole. PTL offers a solution to this problem by being able to escape strings automatically when generating HTML output, at the cost of slightly diminished performance (a few percent). Here's how this feature works. PTL defines a class called ``htmltext`` that represents a string that's already been HTML-escaped and can be safely sent to the client. The function ``htmlescape(string)`` is used to escape data, and it always returns an ``htmltext`` instance. It does nothing if the argument is already ``htmltext``. If a template function is declared ``[html]`` instead of ``[text]`` then two things happen. First, all literal strings in the function become instances of ``htmltext`` instead of Python's ``str``. Second, the values of expressions are passed through ``htmlescape()`` instead of ``str()``. ``htmltext`` type is like the ``str`` type except that operations combining strings and ``htmltext`` instances will result in the string being passed through ``htmlescape()``. For example:: >>> from quixote.html import htmltext >>> htmltext('a') + 'b' >>> 'a' + htmltext('b') >>> htmltext('a%s') % 'b' >>> response = 'green eggs & ham' >>> htmltext('The response was: %s') % response Note that calling ``str()`` strips the ``htmltext`` type and should be avoided since it usually results in characters being escaped more than once. While ``htmltext`` behaves much like a regular string, it is sometimes necessary to insert a ``str()`` inside a template in order to obtain a genuine string. For example, the ``re`` module requires genuine strings. We have found that explicit calls to ``str()`` can often be avoided by splitting some code out of the template into a helper function written in regular Python. It is also recommended that the ``htmltext`` constructor be used as sparingly as possible. The reason is that when using the htmltext feature of PTL, explicit calls to ``htmltext`` become the most likely source of cross-site scripting holes. Calling ``htmltext`` is like saying "I am absolutely sure this piece of data cannot contain malicious HTML code injected by a user. Don't escape HTML special characters because I want them." Note that literal strings in template functions declared with ``[html]`` are htmltext instances, and therefore won't be escaped. You'll only need to use ``htmltext`` when HTML markup comes from outside the template. For example, if you want to include a file containing HTML:: def output_file [html] (): '' # does not get escaped htmltext(open("myfile.html").read()) '' In the common case, templates won't be dealing with HTML markup from external sources, so you can write straightforward code. Consider this function to generate the contents of the ``HEAD`` element:: def meta_tags [html] (title, description): '%s' % title '\n' % description There are no calls to ``htmlescape()`` at all, but string literals such as ``%s`` have all be turned into ``htmltext`` instances, so the string variables will be automatically escaped:: >>> t.meta_tags('Catalog', 'A catalog of our cool products') Catalog \n'> >>> t.meta_tags('Dissertation on ', ... 'Discusses the "LINK" and "META" tags') Dissertation on <HEAD> \n'> >>> Note how the title and description have had HTML-escaping applied to them. (The output has been manually pretty-printed to be more readable.) Once you start using ``htmltext`` in one of your templates, mixing plain and HTML templates is tricky because of ``htmltext``'s automatic escaping; plain templates that generate HTML tags will be double-escaped. One approach is to just use HTML templates throughout your application. Alternatively you can use ``str()`` to convert ``htmltext`` instances to regular Python strings; just be sure the resulting string isn't HTML-escaped again. Two implementations of ``htmltext`` are provided, one written in pure Python and a second one implemented as a C extension. Both versions have seen production use. PTL modules ----------- PTL templates are kept in files with the extension .ptl. Like Python files, they are byte-compiled on import, and the byte-code is written to a compiled file with the extension ``.pyc``. Since vanilla Python doesn't know anything about PTL, Quixote provides an import hook to let you import PTL files just like regular Python modules. The standard way to install this import hook is by calling the ``enable_ptl()`` function:: from quixote import enable_ptl enable_ptl() (Note: if you're using ZODB, always import ZODB *before* installing the PTL import hook. There's some interaction which causes importing the TimeStamp module to fail when the PTL import hook is installed; we haven't debugged the problem. A similar problem has been reported for BioPython and win32com.client imports.) Once the import hook is installed, PTL files can be imported as if they were Python modules. If all the example templates shown here were put into a file named ``foo.ptl``, you could then write Python code that did this:: from foo import numbers def f(): return numbers(10) You may want to keep this little function in your ``PYTHONSTARTUP`` file:: def ptl(): try: import ZODB except ImportError: pass from quixote import enable_ptl enable_ptl() This is useful if you want to interactively play with a PTL module. Quixote-2.7b2/doc/default.css0000664000175000017500000000047511103004231014264 0ustar nasnas/* Cascading style sheet for the Quixote documentation. Just overrides what I don't like about the standard docutils stylesheet. */ @import url(/misc/docutils.css); pre.literal-block, pre.doctest-block { margin-left: 1em ; margin-right: 1em ; background-color: #f4f4f4 } tt { background-color: transparent } Quixote-2.7b2/doc/demo.html0000664000175000017500000003130111103004231013730 0ustar nasnas Running the Quixote Demos

Running the Quixote Demos

Quixote comes with some demonstration applications in the demo directory. After quixote is installed (see INSTALL.txt for instructions), you can run the demos using the scripts located in the server directory.

Each server script is written for a specific method of connecting a quixote publisher to a web server, and you will ultimately want to choose the one that matches your needs. More information about the different server scripts may be found in the scripts themselves and in web-server.txt. To start, though, the easiest way to view the demos is as follows: in a terminal window, run server/simple_server.py, and in a browser, open http://localhost:8080.

The simple_server.py script prints a usage message if you run it with a '--help' command line argument. You can run different demos by using the '--factory' option to identify a callable that creates the publisher you want to use. In particular, you might try these demos:

simple_server.py --factory quixote.demo.mini_demo.create_publisher

or

simple_server.py --factory quixote.demo.altdemo.create_publisher

Understanding the mini_demo

Start the mini demo by running the command:
simple_server.py --factory quixote.demo.mini_demo.create_publisher

In a browser, load http://localhost:8080. In your browser, you should see "Welcome ..." page. In your terminal window, you will see a "localhost - - ..." line for each request. These are access log messages from the web server.

Look at the source code in demo/mini_demo.py. Near the bottom you will find the create_publisher() function. The create_publisher() function creates a Publisher instance whose root directory is an instance of the RootDirectory class defined just above. When a request arrives, the Publisher calls the _q_traverse() method on the root directory. In this case, the RootDirectory is using the standard _q_traverse() implementation, inherited from Directory.

Look, preferably in another window, at the source code for _q_traverse() in directory.py. The path argument provided to _q_traverse() is a list of string components of the path part of the URL, obtained by splitting the request location at each '/' and dropping the first element (which is always '') For example, if the path part of the URL is '/', the path argument to _q_traverse() is ['']. If the path part of the URL is '/a', the path argument to _q_traverse() is ['a']. If the path part of the URL is '/a/', the path argument to _q_traverse() is ['a', ''].

Looking at the code of _q_traverse(), observe that it starts by splitting off the first component of the path and calling _q_translate() to see if there is a designated attribute name corresponding to this component. For the '/' page, the component is '', and _q_translate() returns the attribute name '_q_index'. The _q_traverse() function goes on to lookup the _q_index method and return the result of calling it.

Looking back at mini_demo.py, you can see that the RootDirectory class includes a _q_index() method, and this method does return the HTML for http://localhost:8080/

As mentioned above, the _q_translate() identifies a "designated" attribute name for a given component. The default implementation uses self._q_exports to define this designation. In particular, if the component is in self._q_exports, then it is returned as the attribute name, except in the special case of '', which is translated to the special attribute name '_q_index'.

When you click on the link on the top page, you get http://localhost:8080/hello. In this case, the path argument to the _q_traverse() call is ['hello'], and the return value is the result of calling the hello() method.

Feeling bold? (Just kidding, this won't hurt at all.) Try opening http://localhost:8080/bogus. This is what happens when _q_traverse() raises a TraversalError. A TraversalError is no big deal, but how does quixote handle more exceptional exceptions? To see, you can introduce one by editing mini_demo.py. Try inserting the line "raise 'ouch'" into the hello() method. Kill the demo server (Control-c) and start a new one with the same command as before. Now load the http://localhost:8080/hello page. You should see a plain text python traceback followed by some information extracted from the HTTP request. This information is always printed to the error log on an exception. Here, it is also displayed in the browser because the create_publisher() function made a publisher using the 'plain' value for the display_exceptions keyword argument. If you omit that keyword argument from the Publisher constructor, the browser will get an "Internal Server Error" message instead of the full traceback. If you provide the value 'html', the browser displays a prettier version of the traceback.

One more thing to try here. Replace your 'raise "ouch"' line in the hello() method with 'print "ouch"'. If you restart the server and load the /hello page, you will see that print statements go the the error log (in this case, your terminal window). This can be useful.

Understanding the root demo

Start the root demo by running the command:
simple_server.py --factory quixote.demo.create_publisher

In a browser, open http://localhost:8080 as before. Click around at will.

This is the default demo, but it is more complicated than the mini_demo described above. The create_publisher() function in quixote.demo.__init__.py creates a publisher whose root directory is an instance of quixote.demo.root.RootDirectory. Note that the source code is a file named "root.ptl". The suffix of "ptl" indicates that it is a PTL file, and the import must follow a call to quixote.enable_ptl() or else the source file will not be found or compiled. The quixote.demo.__init__.py file takes care of that.

Take a look at the source code in root.ptl. You will see code that looks like regular python, except that some function definitions have "[html]" between the function name and the parameter list. These functions are ptl templates. For details about PTL, see the PTL.txt file.

This RootDirectory class is similar to the one in mini_demo.py, in that it has a _q_index() method and '' appears in the _q_exports list. One new feature here is the presence of a tuple in the _q_exports list. Most of the time, the elements of the _q_exports lists are just strings that name attributes that should be available as URL components. This pattern does not work, however, when the particular URL component you want to use includes characters (like '.') that can't appear in Python attribute names. To work around these cases, the _q_exports list may contain tuples such as ("favicon.ico", "favicon_ico") to designate "favicon_ico" as the attribute name corresponding the the "favicon.ico" URL component.

Looking at the RootDirectoryMethods, including plain(), css() and favon_ico(), you will see examples where, in addition to returning a string containing the body of the HTTP response, the function also makes side-effect modifications to the response object itself, to set the content type and the expiration time for the response. Most of the time, these direct modifications to the response are not needed. When they are, though, the get_response() function gives you direct access to the response instance.

The RootDirectory here also sets an 'extras' attribute to be an instance of ExtraDirectory, imported from the quixote.demo.extras module. Note that 'extras' also appears in the _q_exports list. This is the ordinary way to extend your URL space through another '/'. For example, the URL path '/extras/' will result in a call to the ExtraDirectory instance's _q_index() method.

The _q_lookup() method

Now take a look at the ExtraDirectory class in extras.ptl. This class exhibits some more advanced publishing features. If you look back at the default _q_traverse() implementation (in directory.py), you will see that the _q_traverse does not give up if _q_translate() returns None, indicating that the path component has no designated corresponding attribute name. In this case, _q_traverse() tries calling self._q_lookup() to see if the object of interest can be found in a different way. Note that _q_lookup() takes the component as an argument and must return either (if there is more path to traverse) a Directory instance, or else (if the component is the last in the path) a callable or a string.

In this particular case, the ExtrasDirectory._q_lookup() call returns an instance of IntegerUI (a subclass of Directory). The interest here, unlike the ExtrasDirectory() instance itself, is created on-the-fly during the traversal, especially for this particular component. Try loading http://localhost:8080/extras/12/ to see how this behaves.

Note that the correct URL to get to the IntegerUI(12)._q_index() call ends with a '/'. This can sometimes be confusing to people who expect http://localhost:8080/extras/12 to yield the same page as http://localhost:8080/extras/12/. If given the path ['extras', '12'], the default _q_traverse() ends up calling the instance of IntegerUI. The Directory.__call__() (see directory.py) determines the result: if no form values were submitted and adding a slash would produce a page, the call returns the result of calling quixote.redirect(). The redirect() call here causes the server to issue a permanent redirect response to the path with the slash added. When this automatic redirect is used, a message is printed to the error log. If the conditions for a redirect are not met, the call falls back to raising a TraversalError. [Note, if you don't like this redirect behavior, override, replace, or delete Directory.__call__]

The _q_lookup() pattern is useful when you want to allow URL components that you either don't know or don't want to list in _q_exports ahead of time.

The _q_resolve() method

Note that the ExtraDirectory class inherits from Resolving (in addition to Directory). The Resolving mixin modifies the _q_traverse() so that, when a component has an attribute name designated by _q_translate(), but the Directory instance does not actually have that attribute, the _q_resolve() method is called to "resolve" the trouble. Typically, the _q_resolve() imports or constructs what should be the value of the designated attribute. The modified _q_translate() sets the attribute value so that the _q_resolve() won't be called again for the same attribute. The _q_resolve() pattern is useful when you want to delay the work of constructing the values for exported attributes.

Forms

You can't get very far writing web applications without writing forms. The root demo includes, at http://localhost:8080/extras/form, a page that demonstrates basic usage of the Form class and widgets defined in the quixote.form package.

Quixote-2.7b2/doc/demo.txt0000664000175000017500000002532011103004231013607 0ustar nasnasRunning the Quixote Demos ========================= Quixote comes with some demonstration applications in the demo directory. After quixote is installed (see INSTALL.txt for instructions), you can run the demos using the scripts located in the server directory. Each server script is written for a specific method of connecting a quixote publisher to a web server, and you will ultimately want to choose the one that matches your needs. More information about the different server scripts may be found in the scripts themselves and in web-server.txt. To start, though, the easiest way to view the demos is as follows: in a terminal window, run server/simple_server.py, and in a browser, open http://localhost:8080. The simple_server.py script prints a usage message if you run it with a '--help' command line argument. You can run different demos by using the '--factory' option to identify a callable that creates the publisher you want to use. In particular, you might try these demos: simple_server.py --factory quixote.demo.mini_demo.create_publisher or simple_server.py --factory quixote.demo.altdemo.create_publisher Understanding the mini_demo --------------------------- Start the mini demo by running the command: simple_server.py --factory quixote.demo.mini_demo.create_publisher In a browser, load http://localhost:8080. In your browser, you should see "Welcome ..." page. In your terminal window, you will see a "localhost - - ..." line for each request. These are access log messages from the web server. Look at the source code in demo/mini_demo.py. Near the bottom you will find the create_publisher() function. The create_publisher() function creates a Publisher instance whose root directory is an instance of the RootDirectory class defined just above. When a request arrives, the Publisher calls the _q_traverse() method on the root directory. In this case, the RootDirectory is using the standard _q_traverse() implementation, inherited from Directory. Look, preferably in another window, at the source code for _q_traverse() in directory.py. The path argument provided to _q_traverse() is a list of string components of the path part of the URL, obtained by splitting the request location at each '/' and dropping the first element (which is always '') For example, if the path part of the URL is '/', the path argument to _q_traverse() is ['']. If the path part of the URL is '/a', the path argument to _q_traverse() is ['a']. If the path part of the URL is '/a/', the path argument to _q_traverse() is ['a', '']. Looking at the code of _q_traverse(), observe that it starts by splitting off the first component of the path and calling _q_translate() to see if there is a designated attribute name corresponding to this component. For the '/' page, the component is '', and _q_translate() returns the attribute name '_q_index'. The _q_traverse() function goes on to lookup the _q_index method and return the result of calling it. Looking back at mini_demo.py, you can see that the RootDirectory class includes a _q_index() method, and this method does return the HTML for http://localhost:8080/ As mentioned above, the _q_translate() identifies a "designated" attribute name for a given component. The default implementation uses self._q_exports to define this designation. In particular, if the component is in self._q_exports, then it is returned as the attribute name, except in the special case of '', which is translated to the special attribute name '_q_index'. When you click on the link on the top page, you get http://localhost:8080/hello. In this case, the path argument to the _q_traverse() call is ['hello'], and the return value is the result of calling the hello() method. Feeling bold? (Just kidding, this won't hurt at all.) Try opening http://localhost:8080/bogus. This is what happens when _q_traverse() raises a TraversalError. A TraversalError is no big deal, but how does quixote handle more exceptional exceptions? To see, you can introduce one by editing mini_demo.py. Try inserting the line "raise 'ouch'" into the hello() method. Kill the demo server (Control-c) and start a new one with the same command as before. Now load the http://localhost:8080/hello page. You should see a plain text python traceback followed by some information extracted from the HTTP request. This information is always printed to the error log on an exception. Here, it is also displayed in the browser because the create_publisher() function made a publisher using the 'plain' value for the display_exceptions keyword argument. If you omit that keyword argument from the Publisher constructor, the browser will get an "Internal Server Error" message instead of the full traceback. If you provide the value 'html', the browser displays a prettier version of the traceback. One more thing to try here. Replace your 'raise "ouch"' line in the hello() method with 'print "ouch"'. If you restart the server and load the /hello page, you will see that print statements go the the error log (in this case, your terminal window). This can be useful. Understanding the root demo --------------------------- Start the root demo by running the command: simple_server.py --factory quixote.demo.create_publisher In a browser, open http://localhost:8080 as before. Click around at will. This is the default demo, but it is more complicated than the mini_demo described above. The create_publisher() function in quixote.demo.__init__.py creates a publisher whose root directory is an instance of quixote.demo.root.RootDirectory. Note that the source code is a file named "root.ptl". The suffix of "ptl" indicates that it is a PTL file, and the import must follow a call to quixote.enable_ptl() or else the source file will not be found or compiled. The quixote.demo.__init__.py file takes care of that. Take a look at the source code in root.ptl. You will see code that looks like regular python, except that some function definitions have "[html]" between the function name and the parameter list. These functions are ptl templates. For details about PTL, see the PTL.txt file. This RootDirectory class is similar to the one in mini_demo.py, in that it has a _q_index() method and '' appears in the _q_exports list. One new feature here is the presence of a tuple in the _q_exports list. Most of the time, the elements of the _q_exports lists are just strings that name attributes that should be available as URL components. This pattern does not work, however, when the particular URL component you want to use includes characters (like '.') that can't appear in Python attribute names. To work around these cases, the _q_exports list may contain tuples such as ("favicon.ico", "favicon_ico") to designate "favicon_ico" as the attribute name corresponding the the "favicon.ico" URL component. Looking at the RootDirectoryMethods, including plain(), css() and favon_ico(), you will see examples where, in addition to returning a string containing the body of the HTTP response, the function also makes side-effect modifications to the response object itself, to set the content type and the expiration time for the response. Most of the time, these direct modifications to the response are not needed. When they are, though, the get_response() function gives you direct access to the response instance. The RootDirectory here also sets an 'extras' attribute to be an instance of ExtraDirectory, imported from the quixote.demo.extras module. Note that 'extras' also appears in the _q_exports list. This is the ordinary way to extend your URL space through another '/'. For example, the URL path '/extras/' will result in a call to the ExtraDirectory instance's _q_index() method. The _q_lookup() method ---------------------- Now take a look at the ExtraDirectory class in extras.ptl. This class exhibits some more advanced publishing features. If you look back at the default _q_traverse() implementation (in directory.py), you will see that the _q_traverse does not give up if _q_translate() returns None, indicating that the path component has no designated corresponding attribute name. In this case, _q_traverse() tries calling self._q_lookup() to see if the object of interest can be found in a different way. Note that _q_lookup() takes the component as an argument and must return either (if there is more path to traverse) a Directory instance, or else (if the component is the last in the path) a callable or a string. In this particular case, the ExtrasDirectory._q_lookup() call returns an instance of IntegerUI (a subclass of Directory). The interest here, unlike the ExtrasDirectory() instance itself, is created on-the-fly during the traversal, especially for this particular component. Try loading http://localhost:8080/extras/12/ to see how this behaves. Note that the correct URL to get to the IntegerUI(12)._q_index() call ends with a '/'. This can sometimes be confusing to people who expect http://localhost:8080/extras/12 to yield the same page as http://localhost:8080/extras/12/. If given the path ['extras', '12'], the default _q_traverse() ends up *calling* the instance of IntegerUI. The Directory.__call__() (see directory.py) determines the result: if no form values were submitted and adding a slash would produce a page, the call returns the result of calling quixote.redirect(). The redirect() call here causes the server to issue a permanent redirect response to the path with the slash added. When this automatic redirect is used, a message is printed to the error log. If the conditions for a redirect are not met, the call falls back to raising a TraversalError. [Note, if you don't like this redirect behavior, override, replace, or delete Directory.__call__] The _q_lookup() pattern is useful when you want to allow URL components that you either don't know or don't want to list in _q_exports ahead of time. The _q_resolve() method ----------------------- Note that the ExtraDirectory class inherits from Resolving (in addition to Directory). The Resolving mixin modifies the _q_traverse() so that, when a component has an attribute name designated by _q_translate(), but the Directory instance does not actually *have* that attribute, the _q_resolve() method is called to "resolve" the trouble. Typically, the _q_resolve() imports or constructs what *should* be the value of the designated attribute. The modified _q_translate() sets the attribute value so that the _q_resolve() won't be called again for the same attribute. The _q_resolve() pattern is useful when you want to delay the work of constructing the values for exported attributes. Forms ----- You can't get very far writing web applications without writing forms. The root demo includes, at http://localhost:8080/extras/form, a page that demonstrates basic usage of the Form class and widgets defined in the quixote.form package. Quixote-2.7b2/doc/form2conversion.html0000664000175000017500000004160111103004231016143 0ustar nasnas Converting form1 forms to use the form2 library

Converting form1 forms to use the form2 library

Note:

The packages names have changed in Quixote 2.

Quixote form1 forms are now in the package quixote.form1. (In Quixote 1, they were in quixote.form.)

Quixote form2 forms are now in the package quixote.form. (In Quixote 1, they were in quixote.form2.)

Introduction

These are some notes and examples for converting Quixote form1 forms, that is forms derived from quixote.form1.Form, to the newer form2 forms.

Form2 forms are more flexible than their form1 counterparts in that they do not require you to use the Form class as a base to get form functionality as form1 forms did. Form2 forms can be instantiated directly and then manipulated as instances. You may also continue to use inheritance for your form2 classes to get form functionality, particularly if the structured separation of process, render, and action is desirable.

There are many ways to get from form1 code ported to form2. At one end of the spectrum is to rewrite the form class using a functional programing style. This method is arguably best since the functional style makes the flow of control clearer.

The other end of the spectrum and normally the easiest way to port form1 forms to form2 is to use the compatibility module provided in the form2 package. The compatibility module's Form class provides much of the same highly structured machinery (via a handle master method) that the form1 framework uses.

Converting form1 forms using using the compatibility module

Here's the short list of things to do to convert form1 forms to form2 using compatibility.

  1. Import the Form base class from quixote.form.compatibility rather than from quixote.form1.

  2. Getting and setting errors is slightly different. In your form's process method, where errors are typically set, form2 has a new interface for marking a widget as having an error.

    Form1 API:

    self.error['widget_name'] = 'the error message'
    

    Form2 API:

    self.set_error('widget_name', 'the error message') 
    

    If you want to find out if the form already has errors, change the form1 style of direct references to the self.errors dictionary to a call to the has_errors method.

    Form1 API:

    if not self.error:
       do some more error checking...
    

    Form2 API:

    if not self.has_errors():
       do some more error checking...
    
  3. Form2 select widgets no longer take allowed_values or descriptions arguments. If you are adding type of form2 select widget, you must provide the options argument instead. Options are the way you define the list of things that are selectable and what is returned when they are selected. the options list can be specified in in one of three ways:

        options: [objects:any]
    or
        options: [(object:any, description:any)]
    or
        options: [(object:any, description:any, key:any)]
    

    An easy way to construct options if you already have allowed_values and descriptions is to use the built-in function zip to define options:

    options=zip(allowed_values, descriptions)
    
Note, however, that often it is simpler to to construct the options list directly.
  1. You almost certainly want to include some kind of cascading style sheet (since form2 forms render with minimal markup). There is a basic set of CSS rules in quixote.form.css.

Here's the longer list of things you may need to tweak in order for form2 compatibility forms to work with your form1 code.

  • widget_type widget class attribute is gone. This means when adding widgets other than widgets defined in quixote.form.widget, you must import the widget class into your module and pass the widget class as the first argument to the add_widget method rather than using the widget_type string.

  • The action_url argument to the form's render method is now a keyword argument.

  • If you use OptionSelectWidget, there is no longer a get_current_option method. You can get the current value in the normal way.

  • ListWidget has been renamed to WidgetList.

  • There is no longer a CollapsibleListWidget class. If you need this functionality, consider writing a 'deletable composite widget' to wrap your WidgetList widgets in it:

    class DeletableWidget(CompositeWidget):
    
      def __init__(self, name, value=None,
                   element_type=StringWidget,
                   element_kwargs={}, **kwargs):
         CompositeWidget.__init__(self, name, value=value, **kwargs)
         self.add(HiddenWidget, 'deleted', value='0')
           if self.get('deleted') != '1':
              self.add(element_type, 'element', value=value,
                       **element_kwargs)
              self.add(SubmitWidget, 'delete', value='Delete')
              if self.get('delete'):
                 self.get_widget('deleted').set_value('1')
    
      def _parse(self, request):
         if self.get('deleted') == '1':
            self.value = None
         else:
            self.value = self.get('element')
    
      def render(self):
         if self.get('deleted') == '1':
            return self.get_widget('deleted').render()
         else:
            return CompositeWidget.render(self)
    

Congratulations, now that you've gotten your form1 forms working in form2, you may wish to simplify this code using some of the new features available in form2 forms. Here's a list of things you may wish to consider:

  • In your process method, you don't really need to get a form_data dictionary by calling Form.process to ensure your widgets are parsed. Instead, the parsed value of any widget is easy to obtain using the widget's get_value method or the form's __getitem__ method. So, instead of:

    form_data = Form.process(self, request)
    val = form_data['my_widget']
    

    You can use:

    val = self['my_widget']
    

    If the widget may or may not be in the form, you can use get:

    val = self.get('my_widget')
    
  • It's normally not necessary to provide the action_url argument to the form's render method.

  • You don't need to save references to your widgets in your form class. You may have a particular reason for wanting to do that, but any widget added to the form using add (or add_widget in the compatibility module) can be retrieved using the form's get_widget method.

Converting form1 forms to form2 by functional rewrite

The best way to get started on a functional version of a form2 rewrite is to look at a trivial example form first written using the form1 inheritance model followed by it's form2 functional equivalent.

First the form1 form:

class MyForm1Form(Form):
    def __init__(self, request, obj):
        Form.__init__(self)

        if obj is None:
            self.obj = Obj()
            self.add_submit_button('add', 'Add')
        else:
            self.obj = obj
            self.add_submit_button('update', 'Update')

        self.add_cancel_button('Cancel', request.get_path(1) + '/')

        self.add_widget('single_select', 'obj_type',
                        title='Object Type',
                        value=self.obj.get_type(),
                        allowed_values=list(obj.VALID_TYPES),
                        descriptions=['type1', 'type2', 'type3'])
        self.add_widget('float', 'cost',
                        title='Cost',
                        value=obj.get_cost())

    def render [html] (self, request, action_url):
        title = 'Obj %s: Edit Object' % self.obj
        header(title)
        Form.render(self, request, action_url)
        footer(title)

    def process(self, request):
        form_data = Form.process(self, request)

        if not self.error:
            if form_data['cost'] is None:
                self.error['cost'] = 'A cost is required.'
            elif form_data['cost'] < 0:
                self.error['cost'] = 'The amount must be positive'
        return form_data

    def action(self, request, submit, form_data):
        self.obj.set_type(form_data['obj_type'])
        self.obj.set_cost(form_data['cost'])
        if submit == 'add':
            db = get_database()
            db.add(self.obj)
        else:
            assert submit == 'update'
        return request.redirect(request.get_path(1) + '/')

Here's the same form using form2 where the function operates on a Form instance it keeps a reference to it as a local variable:

def obj_form(request, obj):
    form = Form() # quixote.form.Form
    if obj is None:
        obj = Obj()
        form.add_submit('add', 'Add')
    else:
        form.add_submit('update', 'Update')
    form.add_submit('cancel', 'Cancel')

    form.add_single_select('obj_type',
                           title='Object Type',
                           value=obj.get_type(),
                           options=zip(obj.VALID_TYPES, 
                                       ['type1', 'type2', 'type3']))
    form.add_float('cost',
                   title='Cost',
                   value=obj.get_cost(),
                   required=1)

    def render [html] ():
        title = 'Obj %s: Edit Object' % obj
        header(title)
        form.render()
        footer(title)

    def process():
       if form['cost'] < 0:
          self.set_error('cost', 'The amount must be positive')

    def action(submit):
      obj.set_type(form['obj_type'])
      obj.set_cost(form['cost'])
      if submit == 'add':
          db = get_database()
          db.add(self.obj)
      else:
          assert submit == 'update'

    exit_path = request.get_path(1) + '/'
    submit = form.get_submit()
    if submit == 'cancel':
        return request.redirect(exit_path)

    if not form.is_submitted() or form.has_errors():
        return render()
    process()
    if form.has_errors():
        return render()
   
    action(submit)
    return request.redirect(exit_path)

As you can see in the example, the function still has all of the same parts of it's form1 equivalent.

  1. It determines if it's to create a new object or edit an existing one
  2. It adds submit buttons and widgets
  3. It has a function that knows how to render the form
  4. It has a function that knows how to do error processing on the form
  5. It has a function that knows how to register permanent changes to objects when the form is submitted successfully.

In the form2 example, we have used inner functions to separate out these parts. This, of course, is optional, but it does help readability once the form gets more complicated and has the additional advantage of mapping directly with it's form1 counterparts.

Form2 functional forms do not have the handle master-method that is called after the form is initialized. Instead, we deal with this functionality manually. Here are some things that the handle portion of your form might need to implement illustrated in the order that often makes sense.

  1. Get the value of any submit buttons using form.get_submit
  2. If the form has not been submitted yet, return render().
  3. See if the cancel button was pressed, if so return a redirect.
  4. Call your process inner function to do any widget-level error checks. The form may already have set some errors, so you may wish to check for that before trying additional error checks.
  5. See if the form was submitted by an unknown submit button. This will be the case if the form was submitted via a JavaScript action, which is the case when an option select widget is selected. The value of get_submit is True in this case and if it is, you want to clear any errors and re-render the form.
  6. If the form has not been submitted or if the form has errors, you simply want to render the form.
  7. Check for your named submit buttons which you expect for successful form posting e.g. add or update. If one of these is pressed, call you action inner function.
  8. Finally, return a redirect to the expected page following a form submission.

These steps are illustrated by the following snippet of code and to a large degree in the above functional form2 code example. Often this handle block of code can be simplified. For example, if you do not expect form submissions from unregistered submit buttons, you can eliminate the test for that. Similarly, if your form does not do any widget-specific error checking, there's no reason to have an error checking process function or the call to it:

exit_path = request.get_path(1) + '/'
submit = form.get_submit()
if not submit:
    return render()
if submit == 'cancel':
    return request.redirect(exit_path)
if submit == True:
    form.clear_errors()
    return render()
process()
if form.has_errors():
    return render()
action(submit)
return request.redirect(exit_path)
Quixote-2.7b2/doc/form2conversion.txt0000664000175000017500000003370611103004231016025 0ustar nasnasConverting form1 forms to use the form2 library =============================================== Note: ----- The packages names have changed in Quixote 2. Quixote form1 forms are now in the package quixote.form1. (In Quixote 1, they were in quixote.form.) Quixote form2 forms are now in the package quixote.form. (In Quixote 1, they were in quixote.form2.) Introduction ------------ These are some notes and examples for converting Quixote form1 forms, that is forms derived from ``quixote.form1.Form``, to the newer form2 forms. Form2 forms are more flexible than their form1 counterparts in that they do not require you to use the ``Form`` class as a base to get form functionality as form1 forms did. Form2 forms can be instantiated directly and then manipulated as instances. You may also continue to use inheritance for your form2 classes to get form functionality, particularly if the structured separation of ``process``, ``render``, and ``action`` is desirable. There are many ways to get from form1 code ported to form2. At one end of the spectrum is to rewrite the form class using a functional programing style. This method is arguably best since the functional style makes the flow of control clearer. The other end of the spectrum and normally the easiest way to port form1 forms to form2 is to use the ``compatibility`` module provided in the form2 package. The compatibility module's Form class provides much of the same highly structured machinery (via a ``handle`` master method) that the form1 framework uses. Converting form1 forms using using the compatibility module ----------------------------------------------------------- Here's the short list of things to do to convert form1 forms to form2 using compatibility. 1. Import the Form base class from ``quixote.form.compatibility`` rather than from quixote.form1. 2. Getting and setting errors is slightly different. In your form's process method, where errors are typically set, form2 has a new interface for marking a widget as having an error. Form1 API:: self.error['widget_name'] = 'the error message' Form2 API:: self.set_error('widget_name', 'the error message') If you want to find out if the form already has errors, change the form1 style of direct references to the ``self.errors`` dictionary to a call to the ``has_errors`` method. Form1 API:: if not self.error: do some more error checking... Form2 API:: if not self.has_errors(): do some more error checking... 3. Form2 select widgets no longer take ``allowed_values`` or ``descriptions`` arguments. If you are adding type of form2 select widget, you must provide the ``options`` argument instead. Options are the way you define the list of things that are selectable and what is returned when they are selected. the options list can be specified in in one of three ways:: options: [objects:any] or options: [(object:any, description:any)] or options: [(object:any, description:any, key:any)] An easy way to construct options if you already have allowed_values and descriptions is to use the built-in function ``zip`` to define options:: options=zip(allowed_values, descriptions) Note, however, that often it is simpler to to construct the ``options`` list directly. 4. You almost certainly want to include some kind of cascading style sheet (since form2 forms render with minimal markup). There is a basic set of CSS rules in ``quixote.form.css``. Here's the longer list of things you may need to tweak in order for form2 compatibility forms to work with your form1 code. * ``widget_type`` widget class attribute is gone. This means when adding widgets other than widgets defined in ``quixote.form.widget``, you must import the widget class into your module and pass the widget class as the first argument to the ``add_widget`` method rather than using the ``widget_type`` string. * The ``action_url`` argument to the form's render method is now a keyword argument. * If you use ``OptionSelectWidget``, there is no longer a ``get_current_option`` method. You can get the current value in the normal way. * ``ListWidget`` has been renamed to ``WidgetList``. * There is no longer a ``CollapsibleListWidget`` class. If you need this functionality, consider writing a 'deletable composite widget' to wrap your ``WidgetList`` widgets in it:: class DeletableWidget(CompositeWidget): def __init__(self, name, value=None, element_type=StringWidget, element_kwargs={}, **kwargs): CompositeWidget.__init__(self, name, value=value, **kwargs) self.add(HiddenWidget, 'deleted', value='0') if self.get('deleted') != '1': self.add(element_type, 'element', value=value, **element_kwargs) self.add(SubmitWidget, 'delete', value='Delete') if self.get('delete'): self.get_widget('deleted').set_value('1') def _parse(self, request): if self.get('deleted') == '1': self.value = None else: self.value = self.get('element') def render(self): if self.get('deleted') == '1': return self.get_widget('deleted').render() else: return CompositeWidget.render(self) Congratulations, now that you've gotten your form1 forms working in form2, you may wish to simplify this code using some of the new features available in form2 forms. Here's a list of things you may wish to consider: * In your process method, you don't really need to get a ``form_data`` dictionary by calling ``Form.process`` to ensure your widgets are parsed. Instead, the parsed value of any widget is easy to obtain using the widget's ``get_value`` method or the form's ``__getitem__`` method. So, instead of:: form_data = Form.process(self, request) val = form_data['my_widget'] You can use:: val = self['my_widget'] If the widget may or may not be in the form, you can use ``get``:: val = self.get('my_widget') * It's normally not necessary to provide the ``action_url`` argument to the form's ``render`` method. * You don't need to save references to your widgets in your form class. You may have a particular reason for wanting to do that, but any widget added to the form using ``add`` (or ``add_widget`` in the compatibility module) can be retrieved using the form's ``get_widget`` method. Converting form1 forms to form2 by functional rewrite ----------------------------------------------------- The best way to get started on a functional version of a form2 rewrite is to look at a trivial example form first written using the form1 inheritance model followed by it's form2 functional equivalent. First the form1 form:: class MyForm1Form(Form): def __init__(self, request, obj): Form.__init__(self) if obj is None: self.obj = Obj() self.add_submit_button('add', 'Add') else: self.obj = obj self.add_submit_button('update', 'Update') self.add_cancel_button('Cancel', request.get_path(1) + '/') self.add_widget('single_select', 'obj_type', title='Object Type', value=self.obj.get_type(), allowed_values=list(obj.VALID_TYPES), descriptions=['type1', 'type2', 'type3']) self.add_widget('float', 'cost', title='Cost', value=obj.get_cost()) def render [html] (self, request, action_url): title = 'Obj %s: Edit Object' % self.obj header(title) Form.render(self, request, action_url) footer(title) def process(self, request): form_data = Form.process(self, request) if not self.error: if form_data['cost'] is None: self.error['cost'] = 'A cost is required.' elif form_data['cost'] < 0: self.error['cost'] = 'The amount must be positive' return form_data def action(self, request, submit, form_data): self.obj.set_type(form_data['obj_type']) self.obj.set_cost(form_data['cost']) if submit == 'add': db = get_database() db.add(self.obj) else: assert submit == 'update' return request.redirect(request.get_path(1) + '/') Here's the same form using form2 where the function operates on a Form instance it keeps a reference to it as a local variable:: def obj_form(request, obj): form = Form() # quixote.form.Form if obj is None: obj = Obj() form.add_submit('add', 'Add') else: form.add_submit('update', 'Update') form.add_submit('cancel', 'Cancel') form.add_single_select('obj_type', title='Object Type', value=obj.get_type(), options=zip(obj.VALID_TYPES, ['type1', 'type2', 'type3'])) form.add_float('cost', title='Cost', value=obj.get_cost(), required=1) def render [html] (): title = 'Obj %s: Edit Object' % obj header(title) form.render() footer(title) def process(): if form['cost'] < 0: self.set_error('cost', 'The amount must be positive') def action(submit): obj.set_type(form['obj_type']) obj.set_cost(form['cost']) if submit == 'add': db = get_database() db.add(self.obj) else: assert submit == 'update' exit_path = request.get_path(1) + '/' submit = form.get_submit() if submit == 'cancel': return request.redirect(exit_path) if not form.is_submitted() or form.has_errors(): return render() process() if form.has_errors(): return render() action(submit) return request.redirect(exit_path) As you can see in the example, the function still has all of the same parts of it's form1 equivalent. 1. It determines if it's to create a new object or edit an existing one 2. It adds submit buttons and widgets 3. It has a function that knows how to render the form 4. It has a function that knows how to do error processing on the form 5. It has a function that knows how to register permanent changes to objects when the form is submitted successfully. In the form2 example, we have used inner functions to separate out these parts. This, of course, is optional, but it does help readability once the form gets more complicated and has the additional advantage of mapping directly with it's form1 counterparts. Form2 functional forms do not have the ``handle`` master-method that is called after the form is initialized. Instead, we deal with this functionality manually. Here are some things that the ``handle`` portion of your form might need to implement illustrated in the order that often makes sense. 1. Get the value of any submit buttons using ``form.get_submit`` 2. If the form has not been submitted yet, return ``render()``. 3. See if the cancel button was pressed, if so return a redirect. 4. Call your ``process`` inner function to do any widget-level error checks. The form may already have set some errors, so you may wish to check for that before trying additional error checks. 5. See if the form was submitted by an unknown submit button. This will be the case if the form was submitted via a JavaScript action, which is the case when an option select widget is selected. The value of ``get_submit`` is ``True`` in this case and if it is, you want to clear any errors and re-render the form. 6. If the form has not been submitted or if the form has errors, you simply want to render the form. 7. Check for your named submit buttons which you expect for successful form posting e.g. ``add`` or ``update``. If one of these is pressed, call you action inner function. 8. Finally, return a redirect to the expected page following a form submission. These steps are illustrated by the following snippet of code and to a large degree in the above functional form2 code example. Often this ``handle`` block of code can be simplified. For example, if you do not expect form submissions from unregistered submit buttons, you can eliminate the test for that. Similarly, if your form does not do any widget-specific error checking, there's no reason to have an error checking ``process`` function or the call to it:: exit_path = request.get_path(1) + '/' submit = form.get_submit() if not submit: return render() if submit == 'cancel': return request.redirect(exit_path) if submit == True: form.clear_errors() return render() process() if form.has_errors(): return render() action(submit) return request.redirect(exit_path) Quixote-2.7b2/doc/multi-threaded.html0000664000175000017500000000346411103004231015725 0ustar nasnas Multi-Threaded Quixote Applications

Multi-Threaded Quixote Applications

Starting with Quixote 0.6, it's possible to write multi-threaded Quixote applications. In previous versions, Quixote stored the current HTTPRequest object in a global variable, meaning that processing multiple requests in the same process simultaneously was impossible.

However, the Publisher class as shipped still can't handle multiple simultaneous requests; you'll need to subclass Publisher to make it re-entrant. Here's a starting point:

import thread
from quixote.publish import Publisher

[...]

class ThreadedPublisher (Publisher):
    def __init__ (self, root_namespace, config=None):
        Publisher.__init__(self, root_namespace, config)
        self._request_dict = {}

    def _set_request(self, request):
        self._request_dict[thread.get_ident()] = request

    def _clear_request(self):
        try:
            del self._request_dict[thread.get_ident()]
        except KeyError:
            pass

    def get_request(self):
        return self._request_dict.get(thread.get_ident())

Using ThreadedPublisher, you now have one current request per thread, rather than one for the entire process.

Quixote-2.7b2/doc/multi-threaded.txt0000664000175000017500000000231211103004231015567 0ustar nasnasMulti-Threaded Quixote Applications =================================== Starting with Quixote 0.6, it's possible to write multi-threaded Quixote applications. In previous versions, Quixote stored the current HTTPRequest object in a global variable, meaning that processing multiple requests in the same process simultaneously was impossible. However, the Publisher class as shipped still can't handle multiple simultaneous requests; you'll need to subclass Publisher to make it re-entrant. Here's a starting point:: import thread from quixote.publish import Publisher [...] class ThreadedPublisher (Publisher): def __init__ (self, root_namespace, config=None): Publisher.__init__(self, root_namespace, config) self._request_dict = {} def _set_request(self, request): self._request_dict[thread.get_ident()] = request def _clear_request(self): try: del self._request_dict[thread.get_ident()] except KeyError: pass def get_request(self): return self._request_dict.get(thread.get_ident()) Using ThreadedPublisher, you now have one current request per thread, rather than one for the entire process. Quixote-2.7b2/doc/programming.html0000664000175000017500000002105211103004231015330 0ustar nasnas Quixote Programming Overview

Quixote Programming Overview

This document explains how a Quixote application is structured. The demo.txt file should probably be read before you read this file. There are three components to a Quixote application:

  1. A driver script, usually a CGI or FastCGI script. This is the interface between your web server (eg., Apache) and the bulk of your application code. The driver script is responsible for creating a Quixote publisher customized for your application and invoking its publishing loop.

  2. A configuration file. This file specifies various features of the Publisher class, such as how errors are handled, the paths of various log files, and various other things. Read through quixote/config.py for the full list of configuration settings.

    The most important configuration parameters are:

    ERROR_EMAIL

    e-mail address to which errors will be mailed

    ERROR_LOG

    file to which errors will be logged

    For development/debugging, you should also set DISPLAY_EXCEPTIONS true; the default value is false, to favor security over convenience.

  3. Finally, the bulk of the code will be called through a call (by the Publisher) to the _q_traverse() method of an instance designated as the root_directory. Normally, the root_directory will be an instance of the Directory class.

Driver script

The driver script is the interface between your web server and Quixote's "publishing loop", which in turn is the gateway to your application code. Thus, there are two things that your Quixote driver script must do:

  • create a Quixote publisher -- that is, an instance of the Publisher class provided by the quixote.publish module -- and customize it for your application
  • invoke the publisher's process_request() method as needed to get responses for one or more requests, writing the responses back to the client(s).

The publisher is responsible for translating URLs to Python objects and calling the appropriate function, method, or PTL template to retrieve the information and/or carry out the action requested by the URL.

The most important application-specific customization done by the driver script is to set the root directory of your application.

The quixote.servers package includes driver modules for cgi, fastcgi, scgi, medusa, twisted, and the simple_server. Each of these modules includes a run() function that you can use in a driver script that provides a function to create the publisher that you want. For an example of this pattern, see the __main__ part of demo/mini_demo.py. You could run the mini_demo.py with scgi by using the run() function imported from quixote.server.scgi_server instead of the one from quixote.server.simple_server. (You would also need your http server set up to use the scgi server.)

That's almost the simplest possible case -- there's no application-specific configuration info apart from the root directory.

Getting the driver script to actually run is between you and your web server. See the web-server.txt document for help.

Configuration file

By default, the Publisher uses the configuration information from quixote/config.py. You should never edit the default values in quixote/config.py, because your edits will be lost if you upgrade to a newer Quixote version. You should certainly read it, though, to understand what all the configuration variables are. If you want to customize any of the configuration variables, your driver script should provide your customized Config instance as an argument to the Publisher constructor.

Logging

The publisher also accepts an optional logger keyword argument, that should, if provided, support the same methods as the default value, an instance of DefaultLogger. Even if you use the default logger, you can still customize the behavior by setting configuration values for access_log, error_log, and/or error_email. These configuration variables are described more fully in config.py.

Quixote writes one (rather long) line to the access log for each request it handles; we have split that line up here to make it easier to read:

127.0.0.1 - 2001-10-15 09:48:43
  2504 "GET /catalog/ HTTP/1.1"
  200 'Opera/6.0 (Linux; U)' 0.10sec

This line consists of:

  • client IP address
  • current user (according to Quixote session management mechanism, so this will be "-" unless you're using a session manager that does authentication)
  • date and time of request in local timezone, as YYYY-MM-DD hh:mm:ss
  • process ID of the process serving the request (eg. your CGI/FastCGI driver script)
  • the HTTP request line (request method, URI, and protocol)
  • response status code
  • HTTP user agent string (specifically, this is repr(os.environ.get('HTTP_USER_AGENT', '')))
  • time to complete the request

If no access log is configured (ie., ACCESS_LOG is None), then Quixote will not do any access logging.

The error log is used for three purposes:

  • application output to sys.stdout and sys.stderr goes to Quixote's error log
  • application tracebacks will be written to Quixote's error log

If no error log is configured (with ERROR_LOG), then all output is redirected to the stderr supplied to Quixote for this request by your web server. At least for CGI/FastCGI scripts under Apache, this winds up in Apache's error log.

Having stdout redirected to the error log is useful for debugging. You can just sprinkle print statements into your application and the output will wind up in the error log.

Application code

Finally, we reach the most complicated part of a Quixote application. However, thanks to Quixote's design, everything you've ever learned about designing and writing Python code is applicable, so there are no new hoops to jump through. You may, optionally, wish to use PTL, which is simply Python with a novel way of generating function return values -- see PTL.txt for details.

Quixote's Publisher constructs a request, splits the path into a list of components, and calls the root directory's _q_traverse() method, giving the component list as an argument. The _q_traverse() will either return a value that will become the content of the HTTPResponse, or else it may raise an Exception. Exceptions are caught by the Publisher and handled as needed, depending on configuration variables and whether or not the Exception is an instance of PublisherError.

Quixote-2.7b2/doc/programming.txt0000664000175000017500000001463411103004231015213 0ustar nasnasQuixote Programming Overview ============================ This document explains how a Quixote application is structured. The demo.txt file should probably be read before you read this file. There are three components to a Quixote application: 1) A driver script, usually a CGI or FastCGI script. This is the interface between your web server (eg., Apache) and the bulk of your application code. The driver script is responsible for creating a Quixote publisher customized for your application and invoking its publishing loop. 2) A configuration file. This file specifies various features of the Publisher class, such as how errors are handled, the paths of various log files, and various other things. Read through quixote/config.py for the full list of configuration settings. The most important configuration parameters are: ``ERROR_EMAIL`` e-mail address to which errors will be mailed ``ERROR_LOG`` file to which errors will be logged For development/debugging, you should also set ``DISPLAY_EXCEPTIONS`` true; the default value is false, to favor security over convenience. 3) Finally, the bulk of the code will be called through a call (by the Publisher) to the _q_traverse() method of an instance designated as the ``root_directory``. Normally, the root_directory will be an instance of the Directory class. Driver script ------------- The driver script is the interface between your web server and Quixote's "publishing loop", which in turn is the gateway to your application code. Thus, there are two things that your Quixote driver script must do: * create a Quixote publisher -- that is, an instance of the Publisher class provided by the quixote.publish module -- and customize it for your application * invoke the publisher's process_request() method as needed to get responses for one or more requests, writing the responses back to the client(s). The publisher is responsible for translating URLs to Python objects and calling the appropriate function, method, or PTL template to retrieve the information and/or carry out the action requested by the URL. The most important application-specific customization done by the driver script is to set the root directory of your application. The quixote.servers package includes driver modules for cgi, fastcgi, scgi, medusa, twisted, and the simple_server. Each of these modules includes a ``run()`` function that you can use in a driver script that provides a function to create the publisher that you want. For an example of this pattern, see the __main__ part of demo/mini_demo.py. You could run the mini_demo.py with scgi by using the ``run()`` function imported from quixote.server.scgi_server instead of the one from quixote.server.simple_server. (You would also need your http server set up to use the scgi server.) That's almost the simplest possible case -- there's no application-specific configuration info apart from the root directory. Getting the driver script to actually run is between you and your web server. See the web-server.txt document for help. Configuration file ------------------ By default, the Publisher uses the configuration information from quixote/config.py. You should never edit the default values in quixote/config.py, because your edits will be lost if you upgrade to a newer Quixote version. You should certainly read it, though, to understand what all the configuration variables are. If you want to customize any of the configuration variables, your driver script should provide your customized Config instance as an argument to the Publisher constructor. Logging ------- The publisher also accepts an optional ``logger`` keyword argument, that should, if provided, support the same methods as the default value, an instance of ``DefaultLogger``. Even if you use the default logger, you can still customize the behavior by setting configuration values for ``access_log``, ``error_log``, and/or ``error_email``. These configuration variables are described more fully in config.py. Quixote writes one (rather long) line to the access log for each request it handles; we have split that line up here to make it easier to read:: 127.0.0.1 - 2001-10-15 09:48:43 2504 "GET /catalog/ HTTP/1.1" 200 'Opera/6.0 (Linux; U)' 0.10sec This line consists of: * client IP address * current user (according to Quixote session management mechanism, so this will be "-" unless you're using a session manager that does authentication) * date and time of request in local timezone, as YYYY-MM-DD hh:mm:ss * process ID of the process serving the request (eg. your CGI/FastCGI driver script) * the HTTP request line (request method, URI, and protocol) * response status code * HTTP user agent string (specifically, this is ``repr(os.environ.get('HTTP_USER_AGENT', ''))``) * time to complete the request If no access log is configured (ie., ``ACCESS_LOG`` is ``None``), then Quixote will not do any access logging. The error log is used for three purposes: * application output to ``sys.stdout`` and ``sys.stderr`` goes to Quixote's error log * application tracebacks will be written to Quixote's error log If no error log is configured (with ``ERROR_LOG``), then all output is redirected to the stderr supplied to Quixote for this request by your web server. At least for CGI/FastCGI scripts under Apache, this winds up in Apache's error log. Having stdout redirected to the error log is useful for debugging. You can just sprinkle ``print`` statements into your application and the output will wind up in the error log. Application code ---------------- Finally, we reach the most complicated part of a Quixote application. However, thanks to Quixote's design, everything you've ever learned about designing and writing Python code is applicable, so there are no new hoops to jump through. You may, optionally, wish to use PTL, which is simply Python with a novel way of generating function return values -- see PTL.txt for details. Quixote's Publisher constructs a request, splits the path into a list of components, and calls the root directory's _q_traverse() method, giving the component list as an argument. The _q_traverse() will either return a value that will become the content of the HTTPResponse, or else it may raise an Exception. Exceptions are caught by the Publisher and handled as needed, depending on configuration variables and whether or not the Exception is an instance of PublisherError. Quixote-2.7b2/doc/session-mgmt.html0000664000175000017500000004402711103004231015442 0ustar nasnas Quixote Session Management

Quixote Session Management

HTTP was originally designed as a stateless protocol, meaning that every request for a document or image was conducted in a separate TCP connection, and that there was no way for a web server to tell if two separate requests actually come from the same user. It's no longer necessarily true that every request is conducted in a separate TCP connection, but HTTP is still fundamentally stateless. However, there are many applications where it is desirable or even essential to establish a "session" for each user, ie. where all requests performed by that user are somehow tied together on the server.

HTTP cookies were invented to address this requirement, and they are still the best solution for establishing sessions on top of HTTP. Thus, the session management mechanism that comes with Quixote is cookie-based. (The most common alternative is to embed the session identifier in the URL. Since Quixote views the URL as a fundamental part of the web user interface, a URL-based session management scheme is considered un-Quixotic.)

For further reading: the standard for cookies that is approximately implemented by most current browsers is RFC 2109; the latest version of the standard is RFC 2965.

In a nutshell, session management with Quixote works like this:

  • when a user-agent first requests a page from a Quixote application that implements session management, Quixote creates a Session object and generates a session ID (a random 64-bit number). The Session object is attached to the current HTTPRequest object, so that application code involved in processing this request has access to the Session object. The get_session() function provides uniform access to the current Session object.

  • if, at the end of processing that request, the application code has stored any information in the Session object, Quixote saves the session in its SessionManager object for use by future requests and sends a session cookie, called QX_session by default, to the user. The session cookie contains the session ID encoded as a hexadecimal string, and is included in the response headers, eg.

    Set-Cookie: QX_session="928F82A9B8FA92FD"
    

    (You can instruct Quixote to specify the domain and path for URLs to which this cookie should be sent.)

  • the user agent stores this cookie for future requests

  • the next time the user agent requests a resource that matches the cookie's domain and path, it includes the QX_session cookie previously generated by Quixote in the request headers, eg.:

    Cookie: QX_session="928F82A9B8FA92FD"
    
  • while processing the request, Quixote decodes the session ID and looks up the corresponding Session object in its SessionManager. If there is no such session, the session cookie is bogus or out-of-date, so Quixote raises SessionError; ultimately the user gets an error page. Otherwise, the Session object is made available, through the get_session() function, as the application code processes the request.

There are two caveats to keep in mind before proceeding, one major and one minor:

  • Quixote's standard Session and SessionManager class do not implement any sort of persistence, meaning that all sessions disappear when the process handling web requests terminates. Thus, session management is completely useless with a plain CGI driver script unless you add some persistence to the mix; see "Session persistence" below for information.
  • Quixote never expires sessions; if you want user sessions to be cleaned up after a period of inactivity, you will have to write code to do it yourself.

Session management demo

There's a simple demo of Quixote's session management in demo/altdemo.py. If the durus (http://www.mems-exchange.org/software/durus/) package is installed, the demo uses a durus database to store sessions, so sessions will be preserved, even if your are running it with plain cgi.

This particular application uses sessions to keep track of just two things: the user's identity and the number of requests made in this session. The first is addressed by Quixote's standard Session class -- every Session object has a user attribute, which you can use for anything you like. In the session demo, we simply store a string, the user's name, which is entered by the user.

Tracking the number of requests is a bit more interesting: from the DemoSession class in altdemo.py:

def __init__ (self, id):
    Session.__init__(self, id)
    self.num_requests = 0

def start_request (self):
    Session.start_request(self)
    self.num_requests += 1

When the session is created, we initialize the request counter; and when we start processing each request, we increment it. Using the session information in the application code is simple. If you want the value of the user attribute of the current session, just call get_user(). If you want some other attribute or method Use get_session() to get the current Session if you need access to other attributes (such as num_requests in the demo) or methods of the current Session instance.

Note that the Session class initializes the user attribute to None, so get_user() will return None if no user has been identified for this session. Application code can use this to change behavior, as in the following:

if not get_user():
    content += htmltext('<p>%s</p>' % href('login', 'login'))
else:
    content += htmltext(
        '<p>Hello, %s.</p>') % get_user()
    content += htmltext('<p>%s</p>' % href('logout', 'logout'))

Note that we must quote the user's name, because they are free to enter anything they please, including special HTML characters like & or <.

Of course, session.user will never be set if we don't set it ourselves. The code that processes the login form is just this (from login() in demo/altdemo.py)

if get_field("name"):
    session = get_session()
    session.set_user(get_field("name")) # This is the important part.

This is obviously a very simple application -- we're not doing any verification of the user's input. We have no user database, no passwords, and no limitations on what constitutes a "user name". A real application would have all of these, as well as a way for users to add themselves to the user database -- ie. register with your web site.

Writing the session class

You will almost certainly have to write a custom session class for your application by subclassing Quixote's standard Session class. Every custom session class has two essential responsibilities:

  • initialize the attributes that will be used by your application
  • override the has_info() method, so the session manager knows when it must save your session object

The first one is fairly obvious and just good practice. The second is essential, and not at all obvious. The has_info() method exists because SessionManager does not automatically hang on to all session objects; this is a defense against clients that ignore cookies, making your session manager create lots of session objects that are just used once. As long as those session objects are not saved, the burden imposed by these clients is not too bad -- at least they aren't sucking up your memory, or bogging down the database that you save session data to. Thus, the session manager uses has_info() to know if it should hang on to a session object or not: if a session has information that must be saved, the session manager saves it and sends a session cookie to the client.

For development/testing work, it's fine to say that your session objects should always be saved:

def has_info (self):
    return 1

The opposite extreme is to forget to override has_info() altogether, in which case session management most likely won't work: unless you tickle the Session object such that the base has_info() method returns true, the session manager won't save the sessions that it creates, and Quixote will never drop a session cookie on the client.

In a real application, you need to think carefully about what data to store in your sessions, and how has_info() should react to the presence of that data. If you try and track something about every single visitor to your site, sooner or later one of those a broken/malicious client that ignores cookies and robots.txt will come along and crawl your entire site, wreaking havoc on your Quixote application (or the database underlying it).

Session persistence

Keeping session data across requests is all very nice, but in the real world you want that data to survive across process termination. With CGI, this is essential, since each process serves exactly one request and then terminates. With other execution mechanisms, though, it's still important -- you don't want to lose all your session data just because your long-lived server process was restarted, or your server machine was rebooted.

However, every application is different, so Quixote doesn't provide any built-in mechanism for session persistence. Instead, it provides a number of hooks, most in the SessionManager class, that let you plug in your preferred persistence mechanism.

The first and most important hook is in the SessionManager constructor: you can provide an alternate mapping object that SessionManager will use to store session objects in. By default, SessionManager uses an ordinary dictionary; if you provide a mapping object that implements persistence, then your session data will automatically persist across processes.

The second hook (two hooks, really) apply if you use a transactional persistence mechanism to provide your SessionManager's mapping. The altdemo.py script does this with Durus, if the durus package is installed, but you could also use ZODB or a relational database for this purpose. The hooks make sure that session (and other) changes get committed or aborted at the appropriate times. SessionManager provides two methods for you to override: forget_changes() and commit_changes(). forget_changes() is called by SessionPublisher whenever a request crashes, ie. whenever your application raises an exception other than PublishError. commit_changes() is called for requests that complete successfully, or that raise a PublishError exception. You'll have to use your own SessionManager subclass if you need to take advantage of these hooks for transactional session persistence.

The third available hook is the Session's is_dirty() method. This is used when your mapping class uses a more primitive storage mechanism, as, for example, the standard 'shelve' module, which provides a mapping object on top of a DBM or Berkeley DB file:

import shelve
sessions = shelve.open("/tmp/quixote-sessions")
session_manager = SessionManager(session_mapping=sessions)

If you use one of these relatively simple persistent mapping types, you'll also need to override is_dirty() in your Session class. That's in addition to overriding has_info(), which determines if a session object is ever saved; is_dirty() is only called on sessions that have already been added to the session mapping, to see if they need to be "re-added". The default implementation always returns false, because once an object has been added to a normal dictionary, there's no need to add it again. However, with simple persistent mapping types like shelve, you need to store the object again each time it changes. Thus, is_dirty() should return true if the session object needs to be re-written. For a simple, naive, but inefficient implementation, making is_dirty an alias for has_info() will work -- that just means that once the session has been written once, it will be re-written on every request.

Quixote-2.7b2/doc/session-mgmt.txt0000664000175000017500000003457611103004231015325 0ustar nasnasQuixote Session Management ========================== HTTP was originally designed as a stateless protocol, meaning that every request for a document or image was conducted in a separate TCP connection, and that there was no way for a web server to tell if two separate requests actually come from the same user. It's no longer necessarily true that every request is conducted in a separate TCP connection, but HTTP is still fundamentally stateless. However, there are many applications where it is desirable or even essential to establish a "session" for each user, ie. where all requests performed by that user are somehow tied together on the server. HTTP cookies were invented to address this requirement, and they are still the best solution for establishing sessions on top of HTTP. Thus, the session management mechanism that comes with Quixote is cookie-based. (The most common alternative is to embed the session identifier in the URL. Since Quixote views the URL as a fundamental part of the web user interface, a URL-based session management scheme is considered un-Quixotic.) For further reading: the standard for cookies that is approximately implemented by most current browsers is RFC 2109; the latest version of the standard is RFC 2965. In a nutshell, session management with Quixote works like this: * when a user-agent first requests a page from a Quixote application that implements session management, Quixote creates a Session object and generates a session ID (a random 64-bit number). The Session object is attached to the current HTTPRequest object, so that application code involved in processing this request has access to the Session object. The get_session() function provides uniform access to the current Session object. * if, at the end of processing that request, the application code has stored any information in the Session object, Quixote saves the session in its SessionManager object for use by future requests and sends a session cookie, called ``QX_session`` by default, to the user. The session cookie contains the session ID encoded as a hexadecimal string, and is included in the response headers, eg. :: Set-Cookie: QX_session="928F82A9B8FA92FD" (You can instruct Quixote to specify the domain and path for URLs to which this cookie should be sent.) * the user agent stores this cookie for future requests * the next time the user agent requests a resource that matches the cookie's domain and path, it includes the ``QX_session`` cookie previously generated by Quixote in the request headers, eg.:: Cookie: QX_session="928F82A9B8FA92FD" * while processing the request, Quixote decodes the session ID and looks up the corresponding Session object in its SessionManager. If there is no such session, the session cookie is bogus or out-of-date, so Quixote raises SessionError; ultimately the user gets an error page. Otherwise, the Session object is made available, through the get_session() function, as the application code processes the request. There are two caveats to keep in mind before proceeding, one major and one minor: * Quixote's standard Session and SessionManager class do not implement any sort of persistence, meaning that all sessions disappear when the process handling web requests terminates. Thus, session management is completely useless with a plain CGI driver script unless you add some persistence to the mix; see "Session persistence" below for information. * Quixote never expires sessions; if you want user sessions to be cleaned up after a period of inactivity, you will have to write code to do it yourself. Session management demo ----------------------- There's a simple demo of Quixote's session management in demo/altdemo.py. If the durus (http://www.mems-exchange.org/software/durus/) package is installed, the demo uses a durus database to store sessions, so sessions will be preserved, even if you are running it with plain cgi. This particular application uses sessions to keep track of just two things: the user's identity and the number of requests made in this session. The first is addressed by Quixote's standard Session class -- every Session object has a ``user`` attribute, which you can use for anything you like. In the session demo, we simply store a string, the user's name, which is entered by the user. Tracking the number of requests is a bit more interesting: from the DemoSession class in altdemo.py:: def __init__ (self, id): Session.__init__(self, id) self.num_requests = 0 def start_request (self): Session.start_request(self) self.num_requests += 1 When the session is created, we initialize the request counter; and when we start processing each request, we increment it. Using the session information in the application code is simple. If you want the value of the user attribute of the current session, just call get_user(). If you want some other attribute or method Use get_session() to get the current Session if you need access to other attributes (such as ``num_requests`` in the demo) or methods of the current Session instance. Note that the Session class initializes the user attribute to None, so get_user() will return None if no user has been identified for this session. Application code can use this to change behavior, as in the following:: if not get_user(): content += htmltext('

%s

' % href('login', 'login')) else: content += htmltext( '

Hello, %s.

') % get_user() content += htmltext('

%s

' % href('logout', 'logout')) Note that we must quote the user's name, because they are free to enter anything they please, including special HTML characters like ``&`` or ``<``. Of course, ``session.user`` will never be set if we don't set it ourselves. The code that processes the login form is just this (from ``login()`` in ``demo/altdemo.py``) :: if get_field("name"): session = get_session() session.set_user(get_field("name")) # This is the important part. This is obviously a very simple application -- we're not doing any verification of the user's input. We have no user database, no passwords, and no limitations on what constitutes a "user name". A real application would have all of these, as well as a way for users to add themselves to the user database -- ie. register with your web site. Configuring the session cookie ------------------------------ Quixote allows you to configure several aspects of the session cookie that it exchanges with clients. First, you can set the name of the cookie; this is important if you have multiple independent Quixote applications running on the same server. For example, the config file for the first application might have :: SESSION_COOKIE_NAME = "foo_session" and the second application might have :: SESSION_COOKIE_NAME = "bar_session" Next, you can use ``SESSION_COOKIE_DOMAIN`` and ``SESSION_COOKIE_PATH`` to set the cookie attributes that control which requests the cookie is included with. By default, these are both ``None``, which instructs Quixote to send the cookie without ``Domain`` or ``Path`` qualifiers. For example, if the client requests ``/foo/bar/`` from www.example.com, and Quixote decides that it must set the session cookie in the response to that request, then the server would send :: Set-Cookie: QX_session="928F82A9B8FA92FD" in the response headers. Since no domain or path were specified with that cookie, the browser will only include the cookie with requests to www.example.com for URIs that start with ``/foo/bar/``. If you want to ensure that your session cookie is included with all requests to www.example.com, you should set ``SESSION_COOKIE_PATH`` in your config file:: SESSION_COOKIE_PATH = "/" which will cause Quixote to set the cookie like this:: Set-Cookie: QX_session="928F82A9B8FA92FD"; Path="/" which will instruct the browser to include that cookie with *all* requests to www.example.com. However, think carefully about what you set ``SESSION_COOKIE_PATH`` to -- eg. if you set it to "/", but all of your Quixote code is under "/q/" in your server's URL-space, then your user's session cookies could be unnecessarily exposed. On shared servers where you don't control all of the code, this is especially dangerous; be sure to use (eg.) :: SESSION_COOKIE_PATH = "/q/" on such servers. The trailing slash is important; without it, your session cookies will be sent to URIs like ``/qux`` and ``/qix``, even if you don't control those URIs. If you want to share the cookie across servers in your domain, eg. www1.example.com and www2.example.com, you'll also need to set ``SESSION_COOKIE_DOMAIN``: SESSION_COOKIE_DOMAIN = ".example.com" Finally, note that the ``SESSION_COOKIE_*`` configuration variables *only* affect Quixote's session cookie; if you set your own cookies using the ``HTTPResponse.set_cookie()`` method, then the cookie sent to the client is completely determined by that ``set_cookie()`` call. See RFCs 2109 and 2965 for more information on the rules browsers are supposed to follow for including cookies with HTTP requests. Writing the session class ------------------------- You will almost certainly have to write a custom session class for your application by subclassing Quixote's standard Session class. Every custom session class has two essential responsibilities: * initialize the attributes that will be used by your application * override the ``has_info()`` method, so the session manager knows when it must save your session object The first one is fairly obvious and just good practice. The second is essential, and not at all obvious. The has_info() method exists because SessionManager does not automatically hang on to all session objects; this is a defense against clients that ignore cookies, making your session manager create lots of session objects that are just used once. As long as those session objects are not saved, the burden imposed by these clients is not too bad -- at least they aren't sucking up your memory, or bogging down the database that you save session data to. Thus, the session manager uses has_info() to know if it should hang on to a session object or not: if a session has information that must be saved, the session manager saves it and sends a session cookie to the client. For development/testing work, it's fine to say that your session objects should always be saved:: def has_info (self): return 1 The opposite extreme is to forget to override ``has_info()`` altogether, in which case session management most likely won't work: unless you tickle the Session object such that the base ``has_info()`` method returns true, the session manager won't save the sessions that it creates, and Quixote will never drop a session cookie on the client. In a real application, you need to think carefully about what data to store in your sessions, and how ``has_info()`` should react to the presence of that data. If you try and track something about every single visitor to your site, sooner or later one of those a broken/malicious client that ignores cookies and ``robots.txt`` will come along and crawl your entire site, wreaking havoc on your Quixote application (or the database underlying it). Session persistence ------------------- Keeping session data across requests is all very nice, but in the real world you want that data to survive across process termination. With CGI, this is essential, since each process serves exactly one request and then terminates. With other execution mechanisms, though, it's still important -- you don't want to lose all your session data just because your long-lived server process was restarted, or your server machine was rebooted. However, every application is different, so Quixote doesn't provide any built-in mechanism for session persistence. Instead, it provides a number of hooks, most in the SessionManager class, that let you plug in your preferred persistence mechanism. The first and most important hook is in the SessionManager constructor: you can provide an alternate mapping object that SessionManager will use to store session objects in. By default, SessionManager uses an ordinary dictionary; if you provide a mapping object that implements persistence, then your session data will automatically persist across processes. The second hook (two hooks, really) apply if you use a transactional persistence mechanism to provide your SessionManager's mapping. The ``altdemo.py`` script does this with Durus, if the durus package is installed, but you could also use ZODB or a relational database for this purpose. The hooks make sure that session (and other) changes get committed or aborted at the appropriate times. SessionManager provides two methods for you to override: ``forget_changes()`` and ``commit_changes()``. ``forget_changes()`` is called by SessionPublisher whenever a request crashes, ie. whenever your application raises an exception other than PublishError. ``commit_changes()`` is called for requests that complete successfully, or that raise a PublishError exception. You'll have to use your own SessionManager subclass if you need to take advantage of these hooks for transactional session persistence. The third available hook is the Session's is_dirty() method. This is used when your mapping class uses a more primitive storage mechanism, as, for example, the standard 'shelve' module, which provides a mapping object on top of a DBM or Berkeley DB file:: import shelve sessions = shelve.open("/tmp/quixote-sessions") session_manager = SessionManager(session_mapping=sessions) If you use one of these relatively simple persistent mapping types, you'll also need to override ``is_dirty()`` in your Session class. That's in addition to overriding ``has_info()``, which determines if a session object is *ever* saved; ``is_dirty()`` is only called on sessions that have already been added to the session mapping, to see if they need to be "re-added". The default implementation always returns false, because once an object has been added to a normal dictionary, there's no need to add it again. However, with simple persistent mapping types like shelve, you need to store the object again each time it changes. Thus, ``is_dirty()`` should return true if the session object needs to be re-written. For a simple, naive, but inefficient implementation, making is_dirty an alias for ``has_info()`` will work -- that just means that once the session has been written once, it will be re-written on every request. Quixote-2.7b2/doc/static-files.html0000664000175000017500000000600311103004231015374 0ustar nasnas Examples of serving static files

Examples of serving static files

The quixote.util module includes classes for making files and directories available as Quixote resources. Here are some examples.

Publishing a Single File

The StaticFile class makes an individual filesystem file (possibly a symbolic link) available. You can also specify the MIME type and encoding of the file; if you don't specify this, the MIME type will be guessed using the standard Python mimetypes.guess_type() function. The default action is to not follow symbolic links, but this behaviour can be changed using the follow_symlinks parameter.

The following example publishes a file with the URL .../stylesheet_css:

# 'stylesheet_css' must be in the _q_exports list
_q_exports = [ ..., 'stylesheet_css', ...]

stylesheet_css = StaticFile(
        "/htdocs/legacy_app/stylesheet.css",
        follow_symlinks=1, mime_type="text/css")

If you want the URL of the file to have a .css extension, you use the external to internal name mapping feature of _q_exports. For example:

_q_exports = [ ..., ('stylesheet.css', 'stylesheet_css'), ...]

Publishing a Directory

Publishing a directory is similar. The StaticDirectory class makes a complete filesystem directory available. Again, the default behaviour is to not follow symlinks. You can also request that the StaticDirectory object cache information about the files in memory so that it doesn't try to guess the MIME type on every hit.

This example publishes the notes/ directory:

_q_exports = [ ..., 'notes', ...]

notes = StaticDirectory("/htdocs/legacy_app/notes")
Quixote-2.7b2/doc/static-files.txt0000664000175000017500000000331011103004231015245 0ustar nasnasExamples of serving static files ================================ The ``quixote.util`` module includes classes for making files and directories available as Quixote resources. Here are some examples. Publishing a Single File ------------------------ The ``StaticFile`` class makes an individual filesystem file (possibly a symbolic link) available. You can also specify the MIME type and encoding of the file; if you don't specify this, the MIME type will be guessed using the standard Python ``mimetypes.guess_type()`` function. The default action is to not follow symbolic links, but this behaviour can be changed using the ``follow_symlinks`` parameter. The following example publishes a file with the URL ``.../stylesheet_css``:: # 'stylesheet_css' must be in the _q_exports list _q_exports = [ ..., 'stylesheet_css', ...] stylesheet_css = StaticFile( "/htdocs/legacy_app/stylesheet.css", follow_symlinks=1, mime_type="text/css") If you want the URL of the file to have a ``.css`` extension, you use the external to internal name mapping feature of ``_q_exports``. For example:: _q_exports = [ ..., ('stylesheet.css', 'stylesheet_css'), ...] Publishing a Directory ---------------------- Publishing a directory is similar. The ``StaticDirectory`` class makes a complete filesystem directory available. Again, the default behaviour is to not follow symlinks. You can also request that the ``StaticDirectory`` object cache information about the files in memory so that it doesn't try to guess the MIME type on every hit. This example publishes the ``notes/`` directory:: _q_exports = [ ..., 'notes', ...] notes = StaticDirectory("/htdocs/legacy_app/notes") Quixote-2.7b2/doc/upgrading.html0000664000175000017500000004374411103004231015002 0ustar nasnas Upgrading code from older versions of Quixote

Upgrading code from older versions of Quixote

This document lists backward-incompatible changes in Quixote, and explains how to update application code to work with the newer version.

Changes from 1.0 to 2.0

Change any imports you have from quixote.form to be from quixote.form1.

Change any imports you have from quixote.form2 to be from quixote.form.

Replace calls to HTTPRequest.get_form_var() with calls to get_field().

Define a create_publisher() function to get the publisher you need and figure out how you want to connect it to web server. See files in demo and server for examples. Note that publish1.py contains a publisher that works more like the Quixote1 Publisher, and does not require the changes listed below.

Make every namespace be an instance of quixote.directory.Directory. Update namespaces that are modules (or in the init.py of a package) by defining a new class in the module that inherits from Directory and moving your _q_exports and _q_* functions onto the class. Replace "request" parameters with "self" parameters on the new methods. If you have a _q_resolve method, include Resolving in the bases of your new class.

Remove request from calls to _q_ functions. If request, session, user, path, or redirect is used in these new methods, replace as needed with calls to get_request(), get_session(), get_user(), get_path(), and/or redirect(), imported from quixote.

In every namespace that formerly traversed into a module, import the new Directory class from the module and create an instance of the Directory in a variable whose name is the name of the module.

In every namespace with a _q_exports and a _q_index, either add "" to _q_exports or make sure that _q_lookup handles "" by returning the result of a call to _q_index.

If your code depends on the Publisher's namespace_stack attribute, try using quixote.util.get_directory_path() instead. If you need the namespace stack after the traversal, override Directory._q_traverse() to call get_directory_path() when the end of the path is reached, and record the result somewhere for later reference.

If your code depends on _q_exception_handler, override the _q_traverse on your root namespace or on your own Directory class to catch exceptions and handle them the way you want. If you just want a general customization for exception responses, you can change or override Publisher.format_publish_error().

If your code depended on _q_access, include the AccessControlled with the bases of your Directory classes as needed.

Provide imports as needed to htmltext, TemplateIO, get_field, get_request, get_session, get_user, get_path, redirect, ?. You may find dulcinea/bin/unknown.py useful for identifying missing imports.

Quixote 1's secure_errors configuration variable is not present in Quixote 2.

Form.__init__ no longer has name or attrs keywords. If your existing code calls Form.__init__ with 'attrs=foo', you'll need to change it to '**foo'. Form instances no longer have a name attribute. If your code looks for form.name, you can find it with form.attrs.get('name'). The Form.__init__ keyword parameter (and attribute) 'action_url' is now named 'action'.

System Message: WARNING/2 (upgrading.txt, line 65); backlink

Inline strong start-string without end-string.

The SessionPublisher class is gone. Use the Publisher class instead. Also, the 'session_mgr' keyword has been renamed to 'session_manager'.

Changes from 0.6.1 to 1.0

Sessions

A leading underscore was removed from the Session attributes __remote_address, __creation_time, and __access_time. If you have pickled Session objects you will need to upgrade them somehow. Our preferred method is to write a script that unpickles each object, renames the attributes and then re-pickles it.

Changes from 0.6 to 0.6.1

_q_exception_handler now called if exception while traversing

_q_exception_handler hooks will now be called if an exception is raised during the traversal process. Quixote 0.6 had a bug that caused _q_exception_handler hooks to only be called if an exception was raised after the traversal completed.

Changes from 0.5 to 0.6

_q_getname renamed to _q_lookup

The _q_getname special function was renamed to _q_lookup, because that name gives a clearer impression of the function's purpose. In 0.6, _q_getname still works but will trigger a warning.

Form Framework Changes

The quixote.form.form module was changed from a .ptl file to a .py file. You should delete or move the existing quixote/ directory in site-packages before running setup.py, or at least delete the old form.ptl and form.ptlc files.

The widget and form classes in the quixote.form package now return htmltext instances. Applications that use forms and widgets will likely have to be changed to use the [html] template type to avoid over-escaping of HTML special characters.

Also, the constructor arguments to SelectWidget and its subclasses have changed. This only affects applications that use the form framework located in the quixote.form package.

In Quixote 0.5, the SelectWidget constructor had this signature:

def __init__ (self, name, value=None,
              allowed_values=None,
              descriptions=None,
              size=None,
              sort=0):

allowed_values was the list of objects that the user could choose, and descriptions was a list of strings that would actually be shown to the user in the generated HTML.

In Quixote 0.6, the signature has changed slightly:

def __init__ (self, name, value=None,
              allowed_values=None,
              descriptions=None,
              options=None,
              size=None,
              sort=0):

The quote argument is gone, and the options argument has been added. If an options argument is provided, allowed_values and descriptions must not be supplied.

The options argument, if present, must be a list of tuples with 1,2, or 3 elements, of the form (value:any, description:any, key:string).

  • value is the object that will be returned if the user chooses this item, and must always be supplied.
  • description is a string or htmltext instance which will be shown to the user in the generated HTML. It will be passed through the htmlescape() functions, so for an ordinary string special characters such as '&' will be converted to '&amp;'. htmltext instances will be left as they are.
  • If supplied, key will be used in the value attribute of the option element (<option value="...">). If not supplied, keys will be generated; value is checked for a _p_oid attribute and if present, that string is used; otherwise the description is used.

In the common case, most applications won't have to change anything, though the ordering of selection items may change due to the difference in how keys are generated.

File Upload Changes

Quixote 0.6 introduces new support for HTTP upload requests. Any HTTP request with a Content-Type of "multipart/form-data" -- which is generally only used for uploads -- is now represented by HTTPUploadRequest, a subclass of HTTPRequest, and the uploaded files themselves are represented by Upload objects.

Whenever an HTTP request has a Content-Type of "multipart/form-data", an instance of HTTPUploadRequest is created instead of HTTPRequest. Some of the fields in the request are presumably uploaded files and might be quite large, so HTTPUploadRequest will read all of the fields supplied in the request body and write them out to temporary files; the temporary files are written in the directory specified by the UPLOAD_DIR configuration variable.

Once the temporary files have been written, the HTTPUploadRequest object is passed to a function or PTL template, just like an ordinary request. The difference between HTTPRequest and HTTPUploadRequest is that all of the form variables are represented as Upload objects. Upload objects have three attributes:

orig_filename
the filename supplied by the browser.
base_filename
a stripped-down version of orig_filename with unsafe characters removed. This could be used when writing uploaded data to a permanent location.
tmp_filename
the path of the temporary file containing the uploaded data for this field.

Consult upload.txt for more information about handling file uploads.

Refactored Publisher Class

Various methods in the Publisher class were rearranged. If your application subclasses Publisher, you may need to change your code accordingly.

  • parse_request() no longer creates the HTTPRequest object; instead a new method, create_request(), handles this, and can be overridden as required.

    As a result, the method signature has changed from parse_request(stdin, env) to parse_request(request).

  • The Publisher.publish() method now catches exceptions raised by parse_request().

Changes from 0.4 to 0.5

Session Management Changes

The Quixote session management interface underwent lots of change and cleanup with Quixote 0.5. It was previously undocumented (apart from docstrings in the code), so we thought that this was a good opportunity to clean up the interface. Nevertheless, those brave souls who got session management working just by reading the code are in for a bit of suffering; this brief note should help clarify things. The definitive documentation for session management is session-mgmt.txt -- you should start there.

Attribute renamings and pickled objects

Most attributes of the standard Session class were made private in order to reduce collisions with subclasses. The downside is that pickled Session objects will break. You might want to (temporarily) modify session.py and add this method to Session:

def __setstate__ (self, dict):
    # Update for attribute renamings made in rev. 1.51.2.3
    # (between Quixote 0.4.7 and 0.5).
    self.__dict__.update(dict)
    if hasattr(self, 'remote_address'):
        self.__remote_address = self.remote_address
        del self.remote_address
    if hasattr(self, 'creation_time'):
        self.__creation_time = self.creation_time
        del self.creation_time
    if hasattr(self, 'access_time'):
        self.__access_time = self.access_time
        del self.access_time
    if hasattr(self, 'form_tokens'):
        self._form_tokens = self.form_tokens
        del self.form_tokens

However, if your sessions were pickled via ZODB, this may not work. (It didn't work for us.) In that case, you'll have to add something like this to your class that inherits from both ZODB's Persistent and Quixote's Session:

def __setstate__ (self, dict):
    # Blechhh!  This doesn't work if I put it in Quixote's
    # session.py, so I have to second-guess how Python
    # treats "__" attribute names.
    self.__dict__.update(dict)
    if hasattr(self, 'remote_address'):
        self._Session__remote_address = self.remote_address
        del self.remote_address
    if hasattr(self, 'creation_time'):
        self._Session__creation_time = self.creation_time
        del self.creation_time
    if hasattr(self, 'access_time'):
        self._Session__access_time = self.access_time
        del self.access_time
    if hasattr(self, 'form_tokens'):
        self._form_tokens = self.form_tokens
        del self.form_tokens

It's not pretty, but it worked for us.

Quixote-2.7b2/doc/upgrading.txt0000664000175000017500000003130711103004231014645 0ustar nasnasUpgrading code from older versions of Quixote ============================================= This document lists backward-incompatible changes in Quixote, and explains how to update application code to work with the newer version. Changes from 1.0 to 2.0 ------------------------- Change any imports you have from quixote.form to be from quixote.form1. Change any imports you have from quixote.form2 to be from quixote.form. Replace calls to HTTPRequest.get_form_var() with calls to get_field(). Define a create_publisher() function to get the publisher you need and figure out how you want to connect it to web server. See files in demo and server for examples. Note that publish1.py contains a publisher that works more like the Quixote1 Publisher, and does not require the changes listed below. Make every namespace be an instance of quixote.directory.Directory. Update namespaces that are modules (or in the init.py of a package) by defining a new class in the module that inherits from Directory and moving your _q_exports and _q_* functions onto the class. Replace "request" parameters with "self" parameters on the new methods. If you have a _q_resolve method, include Resolving in the bases of your new class. Remove request from calls to _q_ functions. If request, session, user, path, or redirect is used in these new methods, replace as needed with calls to get_request(), get_session(), get_user(), get_path(), and/or redirect(), imported from quixote. In every namespace that formerly traversed into a module, import the new Directory class from the module and create an instance of the Directory in a variable whose name is the name of the module. In every namespace with a _q_exports and a _q_index, either add "" to _q_exports or make sure that _q_lookup handles "" by returning the result of a call to _q_index. If your code depends on the Publisher's namespace_stack attribute, try using quixote.util.get_directory_path() instead. If you need the namespace stack after the traversal, override Directory._q_traverse() to call get_directory_path() when the end of the path is reached, and record the result somewhere for later reference. If your code depends on _q_exception_handler, override the _q_traverse on your root namespace or on your own Directory class to catch exceptions and handle them the way you want. If you just want a general customization for exception responses, you can change or override Publisher.format_publish_error(). If your code depended on _q_access, include the AccessControlled with the bases of your Directory classes as needed. Provide imports as needed to htmltext, TemplateIO, get_field, get_request, get_session, get_user, get_path, redirect, ?. You may find dulcinea/bin/unknown.py useful for identifying missing imports. Quixote 1's secure_errors configuration variable is not present in Quixote 2. Form.__init__ no longer has name or attrs keywords. If your existing code calls Form.__init__ with 'attrs=foo', you'll need to change it to '**foo'. Form instances no longer have a name attribute. If your code looks for form.name, you can find it with form.attrs.get('name'). The Form.__init__ keyword parameter (and attribute) 'action_url' is now named 'action'. The SessionPublisher class is gone. Use the Publisher class instead. Also, the 'session_mgr' keyword has been renamed to 'session_manager'. Changes from 0.6.1 to 1.0 ------------------------- Sessions ******** A leading underscore was removed from the ``Session`` attributes ``__remote_address``, ``__creation_time``, and ``__access_time``. If you have pickled ``Session`` objects you will need to upgrade them somehow. Our preferred method is to write a script that unpickles each object, renames the attributes and then re-pickles it. Changes from 0.6 to 0.6.1 ------------------------- ``_q_exception_handler`` now called if exception while traversing ***************************************************************** ``_q_exception_handler`` hooks will now be called if an exception is raised during the traversal process. Quixote 0.6 had a bug that caused ``_q_exception_handler`` hooks to only be called if an exception was raised after the traversal completed. Changes from 0.5 to 0.6 ----------------------- ``_q_getname`` renamed to ``_q_lookup`` *************************************** The ``_q_getname`` special function was renamed to ``_q_lookup``, because that name gives a clearer impression of the function's purpose. In 0.6, ``_q_getname`` still works but will trigger a warning. Form Framework Changes ********************** The ``quixote.form.form`` module was changed from a .ptl file to a .py file. You should delete or move the existing ``quixote/`` directory in ``site-packages`` before running ``setup.py``, or at least delete the old ``form.ptl`` and ``form.ptlc`` files. The widget and form classes in the ``quixote.form`` package now return ``htmltext`` instances. Applications that use forms and widgets will likely have to be changed to use the ``[html]`` template type to avoid over-escaping of HTML special characters. Also, the constructor arguments to ``SelectWidget`` and its subclasses have changed. This only affects applications that use the form framework located in the ``quixote.form`` package. In Quixote 0.5, the ``SelectWidget`` constructor had this signature:: def __init__ (self, name, value=None, allowed_values=None, descriptions=None, size=None, sort=0): ``allowed_values`` was the list of objects that the user could choose, and ``descriptions`` was a list of strings that would actually be shown to the user in the generated HTML. In Quixote 0.6, the signature has changed slightly:: def __init__ (self, name, value=None, allowed_values=None, descriptions=None, options=None, size=None, sort=0): The ``quote`` argument is gone, and the ``options`` argument has been added. If an ``options`` argument is provided, ``allowed_values`` and ``descriptions`` must not be supplied. The ``options`` argument, if present, must be a list of tuples with 1,2, or 3 elements, of the form ``(value:any, description:any, key:string)``. * ``value`` is the object that will be returned if the user chooses this item, and must always be supplied. * ``description`` is a string or htmltext instance which will be shown to the user in the generated HTML. It will be passed through the htmlescape() functions, so for an ordinary string special characters such as '&' will be converted to '&'. htmltext instances will be left as they are. * If supplied, ``key`` will be used in the value attribute of the option element (``')) tags.append(htmltext("")) return htmltext("\n").join(tags) class SingleSelectWidget(SelectWidget): """Widget for single selection. """ SELECT_TYPE = "single_select" MULTIPLE_SELECTION_ERROR = "cannot select multiple values" def _parse(self, request): parsed_key = request.form.get(self.name) if parsed_key: if isinstance(parsed_key, list): self.error = self.MULTIPLE_SELECTION_ERROR else: self.value = self._parse_single_selection(parsed_key) else: self.value = None class RadiobuttonsWidget(SingleSelectWidget): """Widget for a *set* of related radiobuttons -- all have the same name, but different values (and only one of those values is returned by the whole group). Instance attributes: delim : string = None string to emit between each radiobutton in the group. If None, a single newline is emitted. """ SELECT_TYPE = "radiobuttons" def __init__(self, name, value=None, options=None, delim=None, **kwargs): SingleSelectWidget.__init__(self, name, value, options=options, **kwargs) if delim is None: self.delim = "\n" else: self.delim = delim def render_content(self): tags = [] for object, description, key in self.options: if self.is_selected(object): checked = 'checked' else: checked = None r = htmltag("input", xml_end=True, type="radio", name=self.name, value=key, checked=checked, **self.attrs) tags.append(r + htmlescape(description)) return htmlescape(self.delim).join(tags) class MultipleSelectWidget(SelectWidget): """Widget for multiple selection. Instance attributes: value : [any] for multipe selects, the value is None or a list of elements from dict(self.options).values() """ SELECT_TYPE = "multiple_select" def __init__(self, name, value=None, options=None, **kwargs): SelectWidget.__init__(self, name, value, options=options, multiple='multiple', **kwargs) def set_value(self, value): allowed_values = self.get_allowed_values() if value in allowed_values: self.value = [ value ] elif isinstance(value, (list, tuple)): self.value = [ element for element in value if element in allowed_values ] or None else: self.value = None def is_selected(self, value): if self.value is None: return value is None else: return value in self.value def _parse(self, request): parsed_keys = request.form.get(self.name) if parsed_keys: if isinstance(parsed_keys, list): self.value = [value for value, description, key in self.options if key in parsed_keys] or None else: _marker = [] value = self._parse_single_selection(parsed_keys, _marker) if value is _marker: self.value = None else: self.value = [value] else: self.value = None class ButtonWidget(Widget): """ Instance attributes: label : string value : boolean """ HTML_TYPE = "button" def __init__(self, name, value=None, **kwargs): Widget.__init__(self, name, value=None, **kwargs) self.set_label(value) def set_label(self, label): self.label = label def get_label(self): return self.label def render_content(self): # slightly different behavior here, we always render the # tag using the 'value' passed in as a parameter. 'self.value' # is a boolean that is true if the button's name appears # in the request. value = (self.label and htmlescape(self.label) or None) return htmltag("input", xml_end=True, type=self.HTML_TYPE, name=self.name, value=value, **self.attrs) def _parse(self, request): self.value = self.name in request.form class SubmitWidget(ButtonWidget): HTML_TYPE = "submit" class ResetWidget(ButtonWidget): HTML_TYPE = "reset" class HiddenWidget(Widget): """ Instance attributes: value : string """ def set_error(self, error): if error is not None: raise TypeError, 'error not allowed on hidden widgets' def render_content(self): if self.value is None: value = None else: value = htmlescape(self.value) return htmltag("input", xml_end=True, type="hidden", name=self.name, value=value, **self.attrs) def render(self): return self.render_content() # Input elements of type hidden have no decoration. # -- Derived widget types ---------------------------------------------- # (these don't correspond to fundamental widget types in HTML, # so they're separated) class NumberWidget(StringWidget): """ Instance attributes: none """ # Parameterize the number type (either float or int) through # these class attributes: TYPE_OBJECT = None # eg. int, float TYPE_ERROR = None # human-readable error message def __init__(self, name, value=None, **kwargs): assert self.__class__ is not NumberWidget, "abstract class" assert value is None or type(value) is self.TYPE_OBJECT, ( "form value '%s' not a %s: got %r" % (name, self.TYPE_OBJECT, value)) StringWidget.__init__(self, name, value, **kwargs) def _parse(self, request): StringWidget._parse(self, request) if self.value is not None: try: self.value = self.TYPE_OBJECT(self.value) except ValueError: self.error = self.TYPE_ERROR class FloatWidget(NumberWidget): """ Instance attributes: value : float """ TYPE_OBJECT = float TYPE_ERROR = "must be a number" class IntWidget(NumberWidget): """ Instance attributes: value : int """ TYPE_OBJECT = int TYPE_ERROR = "must be an integer" class OptionSelectWidget(SingleSelectWidget): """Widget for single selection with automatic submission. Parse will always return a value from it's options, even if the form is not submitted. This allows its value to be used to decide what other widgets need to be created in a form. It's a powerful feature but it can be hard to understand what's going on. Instance attributes: value : any """ SELECT_TYPE = "option_select" def __init__(self, name, value=None, options=None, **kwargs): SingleSelectWidget.__init__(self, name, value, options=options, onchange='submit()', **kwargs) def parse(self, request=None): if not self._parsed: if request is None: request = get_request() self._parse(request) self._parsed = True return self.value def _parse(self, request): parsed_key = request.form.get(self.name) if parsed_key: if isinstance(parsed_key, list): self.error = self.MULTIPLE_SELECTION_ERROR else: self.value = self._parse_single_selection(parsed_key) elif self.value is None: self.value = self.options[0][0] def render_content(self): return (SingleSelectWidget.render_content(self) + htmltext('')) class CompositeWidget(Widget): """ Instance attributes: widgets : [Widget] _names : {name:string : Widget} """ def __init__(self, name, value=None, **kwargs): Widget.__init__(self, name, value, **kwargs) self.widgets = [] self._names = {} def _parse(self, request): for widget in self.widgets: widget.parse(request) def __getitem__(self, name): return self._names[name].parse() def get(self, name): widget = self._names.get(name) if widget: return widget.parse() return None def get_widget(self, name): return self._names.get(name) def get_widgets(self): return self.widgets def clear_error(self, request=None): Widget.clear_error(self, request) for widget in self.widgets: widget.clear_error(request) def set_widget_error(self, name, error): self._names[name].set_error(error) def has_error(self, request=None): has_error = False if Widget.has_error(self, request=request): has_error = True for widget in self.widgets: if widget.has_error(request=request): has_error = True return has_error def add(self, widget_class, name, *args, **kwargs): if name in self._names: raise ValueError, 'the name %r is already used' % name if self.attrs.get('disabled') and 'disabled' not in kwargs: kwargs['disabled'] = True widget = widget_class(subname(self.name, name), *args, **kwargs) self._names[name] = widget self.widgets.append(widget) def render_content(self): r = TemplateIO(html=True) for widget in self.get_widgets(): r += widget.render() return r.getvalue() class WidgetList(CompositeWidget): """A variable length list of widgets. There is only one title and hint but each element of the list can have its own error. You can also set an error on the WidgetList itself (e.g. as a result of higher-level processing). Instance attributes: element_names : [string] """ def __init__(self, name, value=None, element_type=StringWidget, element_kwargs={}, add_element_label="Add row", **kwargs): assert value is None or type(value) is list, ( "value '%s' not a list: got %r" % (name, value)) assert issubclass(element_type, Widget), ( "value '%s' element_type not a Widget: " "got %r" % (name, element_type)) assert type(element_kwargs) is dict, ( "value '%s' element_kwargs not a dict: " "got %r" % (name, element_kwargs)) assert isinstance(add_element_label, (basestring, htmltext)), ( "value '%s'add_element_label not a string: " "got %r" % (name, add_element_label)) CompositeWidget.__init__(self, name, value, **kwargs) self.element_names = [] self.add(HiddenWidget, 'added_elements') added_elements_widget = self.get_widget('added_elements') def add_element(value=None): name = "element%d" % len(self.element_names) self.add(element_type, name, value=value, **element_kwargs) self.element_names.append(name) # Add element widgets for initial value if value is not None: for element_value in value: add_element(value=element_value) # Add at least one additional element widget num_added = int(added_elements_widget.parse() or 1) for i in range(num_added): add_element() # Add submit to add more element widgets self.add(SubmitWidget, 'add_element', value=add_element_label) if self.get('add_element'): add_element() num_added += 1 added_elements_widget.set_value(num_added) def _parse(self, request): values = [] for name in self.element_names: value = self.get(name) if value is not None: values.append(value) self.value = values or None def render_content(self): r = TemplateIO(html=True) add_element_widget = self.get_widget('add_element') for widget in self.get_widgets(): if widget is add_element_widget: continue r += widget.render() r += add_element_widget.render() return r.getvalue() def render(self): r = TemplateIO(html=True) r += self.render_title(self.get_title()) add_element_widget = self.get_widget('add_element') for widget in self.get_widgets(): if widget is add_element_widget: continue r += widget.render() r += add_element_widget.render() r += self.render_hint(self.get_hint()) return r.getvalue() class WidgetDict(CompositeWidget): """A variable length dict of widgets. There is only one title and hint but each element of the dict can have its own error. You can also set an error on the WidgetDict itself (e.g. as a result of higher-level processing). Instance attributes: element_names : [string] """ def __init__(self, name, value=None, element_key_type=StringWidget, element_value_type=StringWidget, element_key_kwargs={}, element_value_kwargs={}, add_element_label='Add row', **kwargs): assert value is None or type(value) is dict, ( 'value %r not a dict: got %r' % (name, value)) assert issubclass(element_key_type, Widget), ( "value '%s' element_key_type not a Widget: " "got %r" % (name, element_key_type)) assert issubclass(element_value_type, Widget), ( "value '%s' element_value_type not a Widget: " "got %r" % (name, element_value_type)) assert type(element_key_kwargs) is dict, ( "value '%s' element_key_kwargs not a dict: " "got %r" % (name, element_key_kwargs)) assert type(element_value_kwargs) is dict, ( "value '%s' element_value_kwargs not a dict: " "got %r" % (name, element_value_kwargs)) assert isinstance(add_element_label, (basestring, htmltext)), ( 'value %r element_name not a string: ' 'got %r' % (name, add_element_label)) CompositeWidget.__init__(self, name, value, **kwargs) self.element_names = [] self.add(HiddenWidget, 'added_elements') added_elements_widget = self.get_widget('added_elements') def add_element(key=None, value=None): name = 'element%d' % len(self.element_names) self.add(element_key_type, name + 'key', value=key, render_br=False, **element_key_kwargs) self.add(element_value_type, name + 'value', value=value, **element_value_kwargs) self.element_names.append(name) # Add element widgets for initial value if value is not None: for key, element_value in value.items(): add_element(key=key, value=element_value) # Add at least one additional element widget num_added = int(added_elements_widget.parse() or 1) for i in range(num_added): add_element() # Add submit to add more element widgets self.add(SubmitWidget, 'add_element', value=add_element_label) if self.get('add_element'): add_element() num_added += 1 added_elements_widget.set_value(num_added) def _parse(self, request): values = {} for name in self.element_names: key = self.get(name + 'key') value = self.get(name + 'value') if key and value: values[key] = value self.value = values or None def render_content(self): r = TemplateIO(html=True) for name in self.element_names: if name in ('add_element', 'added_elements'): continue key_widget = self.get_widget(name + 'key') value_widget = self.get_widget(name + 'value') r += htmltext('%s
:
%s') % ( key_widget.render(), value_widget.render()) if self.render_br: r += htmltext('
') r += htmltext('\n') r += self.get_widget('add_element').render() r += self.get_widget('added_elements').render() return r.getvalue() Quixote-2.7b2/quixote/form1/0000775000175000017500000000000011326377244014125 5ustar nasnasQuixote-2.7b2/quixote/form1/__init__.py0000664000175000017500000000261511114154100016215 0ustar nasnas"""The web interface framework, consisting of Form and Widget base classes (and a bunch of standard widget classes recognized by Form). Application developers will typically create a Form subclass for each form in their application; each form object will contain a number of widget objects. Custom widgets can be created by inheriting and/or composing the standard widget classes. """ from quixote.form1.form import Form, register_widget_class, FormTokenWidget from quixote.form1.widget import Widget, StringWidget, FileWidget, \ PasswordWidget, TextWidget, CheckboxWidget, RadiobuttonsWidget, \ SingleSelectWidget, SelectWidget, OptionSelectWidget, \ MultipleSelectWidget, ListWidget, SubmitButtonWidget, HiddenWidget, \ FloatWidget, IntWidget, CollapsibleListWidget, FormValueError # Register the standard widget classes register_widget_class(StringWidget) register_widget_class(FileWidget) register_widget_class(PasswordWidget) register_widget_class(TextWidget) register_widget_class(CheckboxWidget) register_widget_class(RadiobuttonsWidget) register_widget_class(SingleSelectWidget) register_widget_class(OptionSelectWidget) register_widget_class(MultipleSelectWidget) register_widget_class(ListWidget) register_widget_class(SubmitButtonWidget) register_widget_class(HiddenWidget) register_widget_class(FloatWidget) register_widget_class(IntWidget) register_widget_class(CollapsibleListWidget) Quixote-2.7b2/quixote/form1/form.py0000664000175000017500000005013011307241201015417 0ustar nasnas"""Provides the Form class and bureaucracy for registering widget classes. (The standard widget classes are registered automatically.) """ from types import StringType from quixote import get_session, get_publisher, redirect from quixote.html import url_quote, htmltag, htmltext, nl2br, TemplateIO from quixote.form1.widget import FormValueError, HiddenWidget class FormTokenWidget (HiddenWidget): def render(self, request): self.value = get_session().create_form_token() return HiddenWidget.render(self, request) JAVASCRIPT_MARKUP = htmltext('''\ ''') class Form: """ A form is the major element of an interactive web page. A form consists of the following: * widgets (input/interaction elements) * text * layout * code to process the form All four of these are the responsibility of Form classes. Typically, you will create one Form subclass for each form in your application. Thanks to the separation of responsibilities here, it's not too hard to structure things so that a given form is rendered and/or processed somewhat differently depending on context. That separation is as follows: * the constructor declares what widgets are in the form, and any static text that is always associated with those widgets (in particular, a widget title and "hint" text) * the 'render()' method combines the widgets and their associated text to create a (1-D) stream of HTML that represents the (2-D) web page that will be presented to the user * the 'process()' method parses the user input values from the form and validates them * the 'action()' method takes care of finishing whatever action was requested by the user submitting the form -- commit a database transaction, update session flags, redirect the user to a new page, etc. This class provides a default 'process()' method that just parses each widget, storing any error messages for display on the next 'render()', and returns the results (if the form parses successfully) in a dictionary. This class also provides a default 'render()' method that lays out widgets and text in a 3-column table: the first column is the widget title, the second column is the widget itself, and the third column is any hint and/or error text associated with the widget. Also provided are methods that can be used to construct this table a row at a time, so you can use this layout for most widgets, but escape from it for oddities. Instance attributes: widgets : { widget_name:string : widget:Widget } dictionary of all widgets in the form widget_order : [Widget] same widgets as 'widgets', but ordered (because order matters) submit_buttons : [SubmitButtonWidget] the submit button widgets in the form error : { widget_name:string : error_message:string } hint : { widget_name:string : hint_text:string } title : { widget_name:string : widget_title:string } required : { widget_name:string : boolean } """ TOKEN_NAME = "_form_id" # name of hidden token widget def __init__(self, method="post", enctype=None, use_tokens=1): if method not in ("post", "get"): raise ValueError("Form method must be 'post' or 'get', " "not %r" % method) self.method = method if enctype is not None and enctype not in ( "application/x-www-form-urlencoded", "multipart/form-data"): raise ValueError, ("Form enctype must be " "'application/x-www-form-urlencoded' or " "'multipart/form-data', not %r" % enctype) self.enctype = enctype # The first major component of a form: its widgets. We want # both easy access and order, so we have a dictionary and a list # of the same objects. The dictionary is keyed on widget name. # These are populated by the 'add_*_widget()' methods. self.widgets = {} self.widget_order = [] self.submit_buttons = [] self.cancel_url = None # The second major component: text. It's up to the 'render()' # method to figure out how to lay these out; the standard # 'render()' does so in a fairly sensible way that should work # for most of our forms. These are also populated by the # 'add_*_widget()' methods. self.error = {} self.hint = {} self.title = {} self.required = {} config = get_publisher().config if self.method == "post" and use_tokens and config.form_tokens: # unique token for each form, this prevents many cross-site # attacks and prevents a form from being submitted twice self.add_widget(FormTokenWidget, self.TOKEN_NAME) self.use_form_tokens = 1 else: self.use_form_tokens = 0 # Subclasses should override this method to specify the actual # widgets in this form -- typically this consists of a series of # calls to 'add_widget()', which updates the data structures we # just defined. # -- Layout (rendering) methods ------------------------------------ # The third major component of a web form is layout. These methods # combine text and widgets in a 1-D stream of HTML, or in a 2-D web # page (depending on your level of abstraction). def render(self, request, action_url): # render(request : HTTPRequest, # action_url : string) # -> HTML text # # Render a form as HTML. assert type(action_url) in (StringType, htmltext) r = TemplateIO(html=1) r += self._render_start(request, action_url, enctype=self.enctype, method=self.method) r += self._render_body(request) r += self._render_finish(request) return r.getvalue() def _render_start(self, request, action, enctype=None, method='post', name=None): r = TemplateIO(html=1) r += htmltag('form', enctype=enctype, method=method, action=action, name=name) r += self._render_hidden_widgets(request) return r.getvalue() def _render_finish(self, request): r = TemplateIO(html=1) r += htmltext('') r += self._render_javascript(request) return r.getvalue() def _render_sep(self, text, line=1): return htmltext('%s%s' '') % \ (line and htmltext('
') or '', text) def _render_error(self, error): if error: return htmltext('%s
') % nl2br(error) else: return '' def _render_hint(self, hint): if hint: return htmltext('%s') % hint else: return '' def _render_widget_row(self, request, widget): if widget.widget_type == 'hidden': return '' title = self.title[widget.name] or '' if self.required.get(widget.name): title = title + htmltext(' *') r = TemplateIO(html=1) r += htmltext('') r += title r += htmltext('' '  ') r += widget.render(request) r += htmltext('') r += self._render_error(self.error.get(widget.name)) r += self._render_hint(self.hint.get(widget.name)) r += htmltext('') return r.getvalue() def _render_hidden_widgets(self, request): r = TemplateIO(html=1) for widget in self.widget_order: if widget.widget_type == 'hidden': r += widget.render(request) r += self._render_error(self.error.get(widget.name)) return r.getvalue() def _render_submit_buttons(self, request, ncols=3): r = TemplateIO(html=1) r += htmltext('\n') % ncols for button in self.submit_buttons: r += button.render(request) r += htmltext('') return r.getvalue() def _render_visible_widgets(self, request): r = TemplateIO(html=1) for widget in self.widget_order: r += self._render_widget_row(request, widget) return r.getvalue() def _render_error_notice(self, request): if self.error: r = htmltext('' 'Warning: ' 'there were errors processing your form. ' 'See below for details.' '') else: r = '' return r def _render_required_notice(self, request): if filter(None, self.required.values()): r = htmltext('' '* = required field' '') else: r = '' return r def _render_body(self, request): r = TemplateIO(html=1) r += htmltext('') r += self._render_error_notice(request) r += self._render_required_notice(request) r += self._render_visible_widgets(request) r += self._render_submit_buttons(request) r += htmltext('
') return r.getvalue() def _render_javascript(self, request): """Render javacript code for the form, if any. Insert code lexically sorted by code_id """ javascript_code = request.response.javascript_code if javascript_code: form_code = [] code_ids = javascript_code.keys() code_ids.sort() for code_id in code_ids: code = javascript_code[code_id] if code: form_code.append(code) javascript_code[code_id] = '' if form_code: return JAVASCRIPT_MARKUP % htmltext(''.join(form_code)) return '' # -- Processing methods -------------------------------------------- # The fourth and final major component: code to process the form. # The standard 'process()' method just parses every widget and # returns a { field_name : field_value } dictionary as 'values'. def process(self, request): """process(request : HTTPRequest) -> values : { string : any } Process the form data, validating all input fields (widgets). If any errors in input fields, adds error messages to the 'error' attribute (so that future renderings of the form will include the errors). Returns a dictionary mapping widget names to parsed values. """ self.error.clear() values = {} for widget in self.widget_order: try: val = widget.parse(request) except FormValueError, exc: self.error[widget.name] = exc.msg else: values[widget.name] = val return values def action(self, request, submit, values): """action(request : HTTPRequest, submit : string, values : { string : any }) -> string Carry out the action required by a form submission. 'submit' is the name of submit button used to submit the form. 'values' is the dictionary of parsed values from 'process()'. Note that error checking cannot be done here -- it must done in the 'process()' method. """ raise NotImplementedError, "sub-classes must implement 'action()'" def handle(self, request): """handle(request : HTTPRequest) -> string Master method for handling forms. It should be called after initializing a form. Controls form action based on a request. You probably should override 'process' and 'action' instead of overriding this method. """ action_url = self.get_action_url(request) if not self.form_submitted(request): return self.render(request, action_url) submit = self.get_submit_button(request) if submit == "cancel": return redirect(self.cancel_url) values = self.process(request) if submit == "": # The form was submitted by unknown submit button, assume that # the submission was required to update the layout of the form. # Clear the errors and re-render the form. self.error.clear() return self.render(request, action_url) if self.use_form_tokens: # before calling action() ensure that there is a valid token # present token = values.get(self.TOKEN_NAME) if not request.session.has_form_token(token): if not self.error: # if there are other errors then don't show the token # error, the form needs to be resubmitted anyhow self.error[self.TOKEN_NAME] = ( "The form you have submitted is invalid. It has " "already been submitted or has expired. Please " "review and resubmit the form.") else: request.session.remove_form_token(token) if self.error: return self.render(request, action_url) else: return self.action(request, submit, values) # -- Convenience methods ------------------------------------------- def form_submitted(self, request): """form_submitted(request : HTTPRequest) -> boolean Return true if a form was submitted in the current request. """ return len(request.form) > 0 def get_action_url(self, request): action_url = url_quote(request.get_path()) query = request.get_environ("QUERY_STRING") if query: action_url += "?" + query return action_url def get_submit_button(self, request): """get_submit_button(request : HTTPRequest) -> string | None Get the name of the submit button that was used to submit the current form. If the browser didn't include this information in the request, use the first submit button registered. """ for button in self.submit_buttons: if button.name in request.form: return button.name else: if request.form and self.submit_buttons: return "" else: return None def get_widget(self, widget_name): return self.widgets.get(widget_name) def parse_widget(self, name, request): """parse_widget(name : string, request : HTTPRequest) -> any Parse the value of named widget. If any parse errors, store the error message (in self.error) for use in the next rendering of the form and return None; otherwise, return the value parsed from the widget (whose type depends on the widget type). """ try: return self.widgets[name].parse(request) except FormValueError, exc: self.error[name] = str(exc) return None def store_value(self, widget_name, request, target, mode="modifier", key=None, missing_error=None): """store_value(widget_name : string, request : HTTPRequest, target : instance | dict, mode : string = "modifier", key : string = widget_name, missing_error : string = None) Parse a widget and, if it parsed successfully, store its value in 'target'. The value is stored in 'target' by name 'key'; if 'key' is not supplied, it defaults to 'widget_name'. How the value is stored depends on 'mode': * modifier: call a modifier method, eg. if 'key' is "foo", call 'target.set_foo(value)' * direct: direct attribute update, eg. if 'key' is "foo" do "target.foo = value" * dict: dictionary update, eg. if 'key' is "foo" do "target['foo'] = value" If 'missing_error' is supplied, use it as an error message if the field doesn't have a value -- ie. supplying 'missing_error' means this field is required. """ value = self.parse_widget(widget_name, request) if (value is None or value == "") and missing_error: self.error[widget_name] = missing_error return None if key is None: key = widget_name if mode == "modifier": # eg. turn "name" into "target.set_name", and # call it like "target.set_name(value)" mod = getattr(target, "set_" + key) mod(value) elif mode == "direct": if not hasattr(target, key): raise AttributeError, \ ("target object %s doesn't have attribute %s" % (`target`, key)) setattr(target, key, value) elif mode == "dict": target[key] = value else: raise ValueError, "unknown update mode %s" % `mode` def clear_widget(self, widget_name): self.widgets[widget_name].clear() def get_widget_value(self, widget_name): return self.widgets[widget_name].value def set_widget_value(self, widget_name, value): self.widgets[widget_name].set_value(value) # -- Form population methods --------------------------------------- def add_widget(self, widget_type, name, value=None, title=None, hint=None, required=0, **args): """add_widget(widget_type : string | Widget, name : string, value : any = None, title : string = None, hint : string = None, required : boolean = 0, ...) -> Widget Create a new Widget object and add it to the form. The widget class used depends on 'widget_type', and the expected type of 'value' also depends on the widget class. Any extra keyword args are passed to the widget constructor. Returns the new Widget. """ if name in self.widgets: raise ValueError, "form already has '%s' variable" % name klass = get_widget_class(widget_type) new_widget = apply(klass, (name, value), args) self.widgets[name] = new_widget self.widget_order.append(new_widget) self.title[name] = title self.hint[name] = hint self.required[name] = required return new_widget def add_submit_button(self, name, value): global _widget_class if name in self.widgets: raise ValueError, "form already has '%s' variable" % name new_widget = _widget_class['submit_button'](name, value) self.widgets[name] = new_widget self.submit_buttons.append(new_widget) def add_cancel_button(self, caption, url): if not isinstance(url, (StringType, htmltext)): raise TypeError, "url must be a string (got %r)" % url self.add_submit_button("cancel", caption) self.cancel_url = url # class Form _widget_class = {} def register_widget_class(klass, widget_type=None): global _widget_class if widget_type is None: widget_type = klass.widget_type assert widget_type is not None, "widget_type must be defined" _widget_class[widget_type] = klass def get_widget_class(widget_type): global _widget_class if hasattr(widget_type, '__call__'): # Presumably someone passed a widget class object to # Widget.create_subwidget() or Form.add_widget() -- # don't bother with the widget class registry at all. return widget_type else: try: return _widget_class[widget_type] except KeyError: raise ValueError("unknown widget type %r" % widget_type) Quixote-2.7b2/quixote/form1/widget.py0000664000175000017500000006277211307241201015756 0ustar nasnas"""Provides the basic web widget classes: Widget itself, plus StringWidget, TextWidget, CheckboxWidget, etc. """ import struct from types import FloatType, IntType, ListType, StringType, TupleType from quixote import get_request from quixote.html import htmltext, htmlescape, htmltag from quixote.http_request import Upload class FormValueError (Exception): """Raised whenever a widget has problems parsing its value.""" def __init__(self, msg): self.msg = msg def __str__(self): return str(self.msg) class Widget: """Abstract base class for web widgets. The key elements of a web widget are: - name - widget type (how the widget looks/works in the browser) - value The name and value are instance attributes (because they're specific to a particular widget in a particular context); widget type is a class attributes. Instance attributes: name : string value : any Feel free to access these directly; to set them, use the 'set_*()' modifier methods. """ # Subclasses must define. 'widget_type' is just a string, e.g. # "string", "text", "checkbox". widget_type = None def __init__(self, name, value=None): assert self.__class__ is not Widget, "abstract class" self.set_name(name) self.set_value(value) def __repr__(self): return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self.name) def __str__(self): return "%s: %s" % (self.widget_type, self.name) def set_name(self, name): self.name = name def set_value(self, value): self.value = value def clear(self): self.value = None # -- Subclasses must implement these ------------------------------- def render(self, request): """render(request) -> HTML text""" raise NotImplementedError def parse(self, request): """parse(request) -> any""" value = request.form.get(self.name) if type(value) is StringType and value.strip(): self.value = value else: self.value = None return self.value # -- Convenience methods for subclasses ---------------------------- # This one's really only for composite widgets; lives here until # we have a demonstrated need for a CompositeWidget class. def get_subwidget_name(self, name): return "%s$%s" % (self.name, name) def create_subwidget(self, widget_type, widget_name, value=None, **args): from quixote.form.form import get_widget_class klass = get_widget_class(widget_type) name = self.get_subwidget_name(widget_name) return apply(klass, (name, value), args) # class Widget # -- Fundamental widget types ------------------------------------------ # These correspond to the standard types of input tag in HTML: # text StringWidget # password PasswordWidget # radio RadiobuttonWidget # checkbox CheckboxWidget # # and also to the other basic form elements: # ")) def parse(self, request): value = Widget.parse(self, request) if value: value = value.replace("\r\n", "\n") self.value = value return self.value class CheckboxWidget (Widget): """Widget for a single checkbox: corresponds to "". Do not put multiple CheckboxWidgets with the same name in the same form. Instance attributes: value : boolean """ widget_type = "checkbox" def render(self, request): return htmltag("input", xml_end=1, type="checkbox", name=self.name, value="yes", checked=self.value and "checked" or None) def parse(self, request): self.value = self.name in request.form return self.value class SelectWidget (Widget): """Widget for single or multiple selection; corresponds to Instance attributes: options : [ (value:any, description:any, key:string) ] value : any The value is None or an element of dict(options.values()). size : int The number of options that should be presented without scrolling. """ # NB. 'widget_type' not set here because this is an abstract class: it's # set by subclasses SingleSelectWidget and MultipleSelectWidget. def __init__(self, name, value=None, allowed_values=None, descriptions=None, options=None, size=None, sort=0, verify_selection=1): assert self.__class__ is not SelectWidget, "abstract class" self.options = [] # if options passed, cannot pass allowed_values or descriptions if allowed_values is not None: assert options is None, ( 'cannot pass both allowed_values and options') assert allowed_values, ( 'cannot pass empty allowed_values list') self.set_allowed_values(allowed_values, descriptions, sort) elif options is not None: assert descriptions is None, ( 'cannot pass both options and descriptions') assert options, ( 'cannot pass empty options list') self.set_options(options, sort) self.set_name(name) self.set_value(value) self.size = size self.verify_selection = verify_selection def get_allowed_values(self): return [item[0] for item in self.options] def get_descriptions(self): return [item[1] for item in self.options] def set_value(self, value): self.value = None for object, description, key in self.options: if value == object: self.value = value break def _generate_keys(self, values, descriptions): """Called if no keys were provided. Try to generate a set of keys that will be consistent between rendering and parsing. """ # try to use ZODB object IDs keys = [] for value in values: if value is None: oid = "" else: oid = getattr(value, "_p_oid", None) if not oid: break hi, lo = struct.unpack(">LL", oid) oid = "%x" % ((hi << 32) | lo) keys.append(oid) else: # found OID for every value return keys # can't use OIDs, try using descriptions used_keys = {} keys = map(str, descriptions) for key in keys: if key in used_keys: raise ValueError, "duplicated descriptions (provide keys)" used_keys[key] = 1 return keys def set_options(self, options, sort=0): """(options: [objects:any], sort=0) or (options: [(object:any, description:any)], sort=0) or (options: [(object:any, description:any, key:any)], sort=0) """ """ Set the options list. The list of options can be a list of objects, in which case the descriptions default to map(htmlescape, objects) applying htmlescape() to each description and key. If keys are provided they must be distinct. If the sort keyword argument is true, sort the options by case-insensitive lexicographic order of descriptions, except that options with value None appear before others. """ if options: first = options[0] values = [] descriptions = [] keys = [] if type(first) is TupleType: if len(first) == 2: for value, description in options: values.append(value) descriptions.append(description) elif len(first) == 3: for value, description, key in options: values.append(value) descriptions.append(description) keys.append(str(key)) else: raise ValueError, 'invalid options %r' % options else: values = descriptions = options if not keys: keys = self._generate_keys(values, descriptions) options = zip(values, descriptions, keys) if sort: def make_sort_key(option): value, description, key = option if value is None: return ('', option) else: return (str(description).lower(), option) doptions = map(make_sort_key, options) doptions.sort() options = [item[1] for item in doptions] self.options = options def parse_single_selection(self, parsed_key): for value, description, key in self.options: if key == parsed_key: return value else: if self.verify_selection: raise FormValueError, "invalid value selected" else: return self.options[0][0] def set_allowed_values(self, allowed_values, descriptions=None, sort=0): """(allowed_values:[any], descriptions:[any], sort:boolean=0) Set the options for this widget. The allowed_values and descriptions parameters must be sequences of the same length. The sort option causes the options to be sorted using case-insensitive lexicographic order of descriptions, except that options with value None appear before others. """ if descriptions is None: self.set_options(allowed_values, sort) else: assert len(descriptions) == len(allowed_values) self.set_options(zip(allowed_values, descriptions), sort) def is_selected(self, value): return value == self.value def render(self, request): if self.widget_type == "multiple_select": multiple = "multiple" else: multiple = None if self.widget_type == "option_select": onchange = "submit()" else: onchange = None tags = [htmltag("select", name=self.name, multiple=multiple, onchange=onchange, size=self.size)] for object, description, key in self.options: if self.is_selected(object): selected = "selected" else: selected = None if description is None: description = "" r = htmltag("option", value=key, selected=selected) tags.append(r + htmlescape(description) + htmltext('')) tags.append(htmltext("")) return htmltext("\n").join(tags) class SingleSelectWidget (SelectWidget): """Widget for single selection. """ widget_type = "single_select" def parse(self, request): parsed_key = request.form.get(self.name) self.value = None if parsed_key: if type(parsed_key) is ListType: raise FormValueError, "cannot select multiple values" self.value = self.parse_single_selection(parsed_key) return self.value class RadiobuttonsWidget (SingleSelectWidget): """Widget for a *set* of related radiobuttons -- all have the same name, but different values (and only one of those values is returned by the whole group). Instance attributes: delim : string = None string to emit between each radiobutton in the group. If None, a single newline is emitted. """ widget_type = "radiobuttons" def __init__(self, name, value=None, allowed_values=None, descriptions=None, options=None, delim=None): SingleSelectWidget.__init__(self, name, value, allowed_values, descriptions, options) if delim is None: self.delim = "\n" else: self.delim = delim def render(self, request): tags = [] for object, description, key in self.options: if self.is_selected(object): checked = "checked" else: checked = None r = htmltag("input", xml_end=True, type="radio", name=self.name, value=key, checked=checked) tags.append(r + htmlescape(description)) return htmlescape(self.delim).join(tags) class MultipleSelectWidget (SelectWidget): """Widget for multiple selection. Instance attributes: value : [any] for multipe selects, the value is None or a list of elements from dict(self.options).values() """ widget_type = "multiple_select" def set_value(self, value): allowed_values = self.get_allowed_values() if value in allowed_values: self.value = [ value ] elif type(value) in (ListType, TupleType): self.value = [ element for element in value if element in allowed_values ] or None else: self.value = None def is_selected(self, value): if self.value is None: return value is None else: return value in self.value def parse(self, request): parsed_keys = request.form.get(self.name) self.value = None if parsed_keys: if type(parsed_keys) is ListType: self.value = [value for value, description, key in self.options if key in parsed_keys] or None else: self.value = [self.parse_single_selection(parsed_keys)] return self.value class SubmitButtonWidget (Widget): """ Instance attributes: value : boolean """ widget_type = "submit_button" def __init__(self, name=None, value=None): Widget.__init__(self, name, value) def render(self, request): value = (self.value and htmlescape(self.value) or None) return htmltag("input", xml_end=1, type="submit", name=self.name, value=value) def parse(self, request): return request.form.get(self.name) def is_submitted(self): return self.parse(get_request()) class HiddenWidget (Widget): """ Instance attributes: value : string """ widget_type = "hidden" def render(self, request): if self.value is None: value = None else: value = htmlescape(self.value) return htmltag("input", xml_end=1, type="hidden", name=self.name, value=value) def set_current_value(self, value): self.value = value request = get_request() if request.form: request.form[self.name] = value def get_current_value(self): request = get_request() if request.form: return self.parse(request) else: return self.value # -- Derived widget types ---------------------------------------------- # (these don't correspond to fundamental widget types in HTML, # so they're separated) class NumberWidget (StringWidget): """ Instance attributes: none """ # Parameterize the number type (either float or int) through # these class attributes: type_object = None # eg. int, float type_error = None # human-readable error message type_converter = None # eg. int(), float() def __init__(self, name, value=None, size=None, maxlength=None): assert self.__class__ is not NumberWidget, "abstract class" assert value is None or type(value) is self.type_object, ( "form value '%s' not a %s: got %r" % (name, self.type_object, value)) StringWidget.__init__(self, name, value, size, maxlength) def parse(self, request): value = StringWidget.parse(self, request) if value: try: self.value = self.type_converter(value) except ValueError: raise FormValueError, self.type_error return self.value class FloatWidget (NumberWidget): """ Instance attributes: value : float """ widget_type = "float" type_object = FloatType type_converter = float type_error = "must be a number" class IntWidget (NumberWidget): """ Instance attributes: value : int """ widget_type = "int" type_object = IntType type_converter = int type_error = "must be an integer" class OptionSelectWidget (SingleSelectWidget): """Widget for single selection with automatic submission and early parsing. This widget parses the request when it is created. This allows its value to be used to decide what other widgets need to be created in a form. It's a powerful feature but it can be hard to understand what's going on. Instance attributes: value : any """ widget_type = "option_select" def __init__(self, *args, **kwargs): SingleSelectWidget.__init__(self, *args, **kwargs) request = get_request() if request.form: SingleSelectWidget.parse(self, request) if self.value is None: self.value = self.options[0][0] def render(self, request): return (SingleSelectWidget.render(self, request) + htmltext('')) def parse(self, request): return self.value def get_current_option(self): return self.value class ListWidget (Widget): """Widget for lists of objects. Instance attributes: value : [any] """ widget_type = "list" def __init__(self, name, value=None, element_type=None, element_name="row", **args): assert value is None or type(value) is ListType, ( "form value '%s' not a list: got %r" % (name, value)) assert type(element_name) in (StringType, htmltext), ( "form value '%s' element_name not a string: " "got %r" % (name, element_name)) Widget.__init__(self, name, value) if element_type is None: self.element_type = "string" else: self.element_type = element_type self.args = args self.added_elements_widget = self.create_subwidget( "hidden", "added_elements") added_elements = int(self.added_elements_widget.get_current_value() or '1') self.add_button = self.create_subwidget( "submit_button", "add_element", value="Add %s" % element_name) if self.add_button.is_submitted(): added_elements += 1 self.added_elements_widget.set_current_value(str(added_elements)) self.element_widgets = [] self.element_count = 0 if self.value is not None: for element in self.value: self.add_element(element) for index in range(added_elements): self.add_element() def add_element(self, value=None): self.element_widgets.append( self.create_subwidget(self.element_type, "element_%d" % self.element_count, value=value, **self.args)) self.element_count += 1 def render(self, request): tags = [] for element_widget in self.element_widgets: tags.append(element_widget.render(request)) tags.append(self.add_button.render(request)) tags.append(self.added_elements_widget.render(request)) return htmltext('
\n').join(tags) def parse(self, request): self.value = [] for element_widget in self.element_widgets: value = element_widget.parse(request) if value is not None: self.value.append(value) self.value = self.value or None return self.value class CollapsibleListWidget (ListWidget): """Widget for lists of objects with associated delete buttons. CollapsibleListWidget behaves like ListWidget except that each element is rendered with an associated delete button. Pressing the delete button will cause the associated element name to be added to a hidden widget that remembers all deletions until the form is submitted. Only elements that are not marked as deleted will be rendered and ultimately added to the value of the widget. Instance attributes: value : [any] """ widget_type = "collapsible_list" def __init__(self, name, value=None, element_name="row", **args): self.name = name self.element_name = element_name self.deleted_elements_widget = self.create_subwidget( "hidden", "deleted_elements") self.element_delete_buttons = [] self.deleted_elements = ( self.deleted_elements_widget.get_current_value() or '') ListWidget.__init__(self, name, value=value, element_name=element_name, **args) def add_element(self, value=None): element_widget_name = "element_%d" % self.element_count if self.deleted_elements.find(element_widget_name) == -1: delete_button = self.create_subwidget( "submit_button", "delete_" + element_widget_name, value="Delete %s" % self.element_name) if delete_button.is_submitted(): self.element_count += 1 self.deleted_elements += element_widget_name self.deleted_elements_widget.set_current_value( self.deleted_elements) else: self.element_delete_buttons.append(delete_button) ListWidget.add_element(self, value=value) else: self.element_count += 1 def render(self, request): tags = [] for element_widget, element_delete_button in zip( self.element_widgets, self.element_delete_buttons): if self.deleted_elements.find(element_widget.name) == -1: tags.append(element_widget.render(request) + element_delete_button.render(request)) tags.append(self.add_button.render(request)) tags.append(self.added_elements_widget.render(request)) tags.append(self.deleted_elements_widget.render(request)) return htmltext('
\n').join(tags) Quixote-2.7b2/quixote/html/0000775000175000017500000000000011326377244014045 5ustar nasnasQuixote-2.7b2/quixote/html/test/0000775000175000017500000000000011326377244015024 5ustar nasnasQuixote-2.7b2/quixote/html/test/utest_html.py0000775000175000017500000002732711114154100017557 0ustar nasnasimport sys from sancho.utest import UTest from quixote.html import _py_htmltext from quixote.html import href, url_with_query, url_quote, nl2br markupchars = '<>&"' quotedchars = '<>&"' if sys.hexversion >= 0x20400a2: unicodechars = u'\u1234' else: unicodechars = 'x' # lie, Python <= 2.3 is broken class Wrapper: def __init__(self, s): self.s = s def __repr__(self): return self.s def __str__(self): return self.s class BrokenError(Exception): pass class Broken: def __str__(self): raise BrokenError, 'eieee' def __repr__(self): raise BrokenError, 'eieee' htmltext = escape = htmlescape = TemplateIO = stringify = None class HTMLTest (UTest): def check_href(self): assert str(href('/foo/bar', 'bar')) == 'bar' def check_url_with_query(self): assert str(url_with_query('/f/b', a='1')) == '/f/b?a=1' assert str(url_with_query( '/f/b', a='1', b='3 4')) == '/f/b?a=1&b=3%204' def check_nl2br(self): assert str(nl2br('a\nb\nc')) == 'a
\nb
\nc' def check_url_quote(self): assert url_quote('abc') == 'abc' assert url_quote('a b c') == 'a%20b%20c' assert url_quote(None, fallback='abc') == 'abc' class HTMLTextTest (UTest): def _pre(self): global htmltext, escape, htmlescape, TemplateIO, stringify htmltext = _py_htmltext.htmltext escape = _py_htmltext._escape_string stringify = _py_htmltext.stringify htmlescape = _py_htmltext.htmlescape TemplateIO = _py_htmltext.TemplateIO def _post(self): global htmltext, escape, htmlescape, TemplateIO, stringify htmltext = escape = htmlescape = TemplateIO = stringify = None def _check_init(self): assert str(htmltext('foo')) == 'foo' assert str(htmltext(markupchars)) == markupchars assert unicode(htmltext(unicodechars)) == unicodechars assert str(htmltext(unicode(markupchars))) == markupchars assert str(htmltext(None)) == 'None' assert str(htmltext(1)) == '1' try: htmltext(Broken()) assert 0 except BrokenError: pass def check_stringify(self): assert stringify(markupchars) is markupchars assert stringify(unicodechars) is unicodechars assert stringify(Wrapper(unicodechars)) is unicodechars assert stringify(Wrapper(markupchars)) is markupchars assert stringify(Wrapper) == str(Wrapper) assert stringify(None) == str(None) def check_escape(self): assert htmlescape(markupchars) == quotedchars assert isinstance(htmlescape(markupchars), htmltext) assert escape(markupchars) == quotedchars assert escape(unicodechars) == unicodechars assert escape(unicode(markupchars)) == quotedchars assert isinstance(escape(markupchars), basestring) assert htmlescape(htmlescape(markupchars)) == quotedchars try: escape(1) assert 0 except TypeError: pass def check_cmp(self): s = htmltext("foo") assert s == 'foo' assert s != 'bar' assert s == htmltext('foo') assert s != htmltext('bar') assert htmltext(u'\u1234') == u'\u1234' assert htmltext('1') != 1 assert 1 != s def check_len(self): assert len(htmltext('foo')) == 3 assert len(htmltext(markupchars)) == len(markupchars) assert len(htmlescape(markupchars)) == len(quotedchars) def check_hash(self): assert hash(htmltext('foo')) == hash('foo') assert hash(htmltext(markupchars)) == hash(markupchars) assert hash(htmlescape(markupchars)) == hash(quotedchars) def check_concat(self): s = htmltext("foo") assert s + 'bar' == "foobar" assert 'bar' + s == "barfoo" assert s + htmltext('bar') == "foobar" assert s + markupchars == "foo" + quotedchars assert isinstance(s + markupchars, htmltext) assert markupchars + s == quotedchars + "foo" assert isinstance(markupchars + s, htmltext) assert markupchars + htmltext(u'') == quotedchars try: s + 1 assert 0 except TypeError: pass try: 1 + s assert 0 except TypeError: pass # mixing unicode and str assert repr(htmltext('a') + htmltext('b')) == "" assert repr(htmltext(u'a') + htmltext('b')) == "" assert repr(htmltext('a') + htmltext(u'b')) == "" def check_repeat(self): s = htmltext('a') assert s * 3 == "aaa" assert isinstance(s * 3, htmltext) assert htmlescape(markupchars) * 3 == quotedchars * 3 try: s * 'a' assert 0 except TypeError: pass try: 'a' * s assert 0 except TypeError: pass try: s * s assert 0 except TypeError: pass def check_format(self): s_fmt = htmltext('%s') u_fmt = htmltext(u'%s') assert s_fmt % 'foo' == "foo" assert u_fmt % 'foo' == u"foo" assert isinstance(s_fmt % 'foo', htmltext) assert isinstance(u_fmt % 'foo', htmltext) assert s_fmt % markupchars == quotedchars assert u_fmt % markupchars == quotedchars assert s_fmt % None == "None" assert u_fmt % None == "None" assert s_fmt % unicodechars == unicodechars assert u_fmt % unicodechars == unicodechars assert s_fmt % htmltext(unicodechars) == unicodechars assert u_fmt % htmltext(unicodechars) == unicodechars assert htmltext('%r') % Wrapper(markupchars) == quotedchars assert htmltext('%r') % unicodechars == `unicodechars` assert htmltext('%s%s') % ('foo', htmltext(markupchars)) \ == ("foo" + markupchars) assert htmltext('%d') % 10 == "10" assert htmltext('%.1f') % 10 == "10.0" try: s_fmt % Broken() assert 0 except BrokenError: pass try: htmltext('%r') % Broken() assert 0 except BrokenError: pass try: s_fmt % (1, 2) assert 0 except TypeError: pass assert htmltext('%d') % 12300000000000000000L == "12300000000000000000" def check_dict_format(self): args = {'a': 'foo&', 'b': htmltext('bar&')} result = "foo& 'foo&' bar&" assert htmltext('%(a)s %(a)r %(b)s') % args == result assert htmltext('%(a)s') % {'a': 'foo&'} == "foo&" assert isinstance(htmltext('%(a)s') % {'a': 'a'}, htmltext) assert htmltext('%s') % {'a': 'foo&'} == "{'a': 'foo&'}" try: htmltext('%(a)s') % 1 assert 0 except TypeError: pass try: htmltext('%(a)s') % {} assert 0 except KeyError: pass assert htmltext('') % {} == '' assert htmltext('%%') % {} == '%' def check_join(self): assert htmltext(' ').join(['foo', 'bar']) == "foo bar" assert htmltext(' ').join(['foo', markupchars]) == \ "foo " + quotedchars assert htmlescape(markupchars).join(['foo', 'bar']) == \ "foo" + quotedchars + "bar" assert htmltext(' ').join([htmltext(markupchars), 'bar']) == \ markupchars + " bar" assert isinstance(htmltext('').join([]), htmltext) assert htmltext(u' ').join([unicodechars]) == unicodechars assert htmltext(u' ').join(['']) == u'' try: htmltext('').join(1) assert 0 except TypeError: pass try: htmltext('').join([1]) assert 0 except TypeError: pass def check_startswith(self): assert htmltext('foo').startswith('fo') assert htmlescape(markupchars).startswith(markupchars[:3]) assert htmltext(markupchars).startswith(htmltext(markupchars[:3])) try: htmltext('').startswith(1) assert 0 except TypeError: pass def check_endswith(self): assert htmltext('foo').endswith('oo') assert htmlescape(markupchars).endswith(markupchars[-3:]) assert htmltext(markupchars).endswith(htmltext(markupchars[-3:])) try: htmltext('').endswith(1) assert 0 except TypeError: pass def check_replace(self): assert htmlescape('&').replace('&', 'foo') == "foo" assert htmltext('&').replace(htmltext('&'), 'foo') == "foo" assert htmltext('foo').replace('foo', htmltext('&')) == "&" assert isinstance(htmltext('a').replace('a', 'b'), htmltext) try: htmltext('').replace(1, 'a') assert 0 except TypeError: pass def check_lower(self): assert htmltext('aB').lower() == "ab" assert isinstance(htmltext('a').lower(), htmltext) def check_upper(self): assert htmltext('aB').upper() == "AB" assert isinstance(htmltext('a').upper(), htmltext) def check_capitalize(self): assert htmltext('aB').capitalize() == "Ab" assert isinstance(htmltext('a').capitalize(), htmltext) class TemplateTest (UTest): def _pre(self): global TemplateIO TemplateIO = _py_htmltext.TemplateIO def _post(self): global TemplateIO TemplateIO = None def check_init(self): TemplateIO() TemplateIO(html=True) TemplateIO(html=False) def check_text_iadd(self): t = TemplateIO() assert t.getvalue() == '' t += "abcd" assert t.getvalue() == 'abcd' t += None assert t.getvalue() == 'abcd' t += 123 assert t.getvalue() == 'abcd123' t += u'\u1234' assert t.getvalue() == u'abcd123\u1234' try: t += Broken(); t.getvalue() assert 0 except BrokenError: pass def check_html_iadd(self): t = TemplateIO(html=1) assert t.getvalue() == '' t += "abcd" assert t.getvalue() == 'abcd' t += None assert t.getvalue() == 'abcd' t += 123 assert t.getvalue() == 'abcd123' try: t += Broken(); t.getvalue() assert 0 except BrokenError: pass t = TemplateIO(html=1) t += markupchars assert t.getvalue() == quotedchars def check_repr(self): t = TemplateIO() t += "abcd" assert "TemplateIO" in repr(t) def check_str(self): t = TemplateIO() t += "abcd" assert str(t) == "abcd" try: from quixote.html import _c_htmltext except ImportError: _c_htmltext = None if _c_htmltext: class CHTMLTest(HTMLTest): def _pre(self): # using globals like this is a bit of a hack since it assumes # Sancho tests each class individually, oh well global htmltext, escape, htmlescape, stringify htmltext = _c_htmltext.htmltext escape = _c_htmltext._escape_string stringify = _py_htmltext.stringify htmlescape = _c_htmltext.htmlescape class CHTMLTextTest(HTMLTextTest): def _pre(self): global htmltext, escape, htmlescape, stringify htmltext = _c_htmltext.htmltext escape = _c_htmltext._escape_string stringify = _py_htmltext.stringify htmlescape = _c_htmltext.htmlescape class CTemplateTest(TemplateTest): def _pre(self): global TemplateIO TemplateIO = _c_htmltext.TemplateIO if __name__ == "__main__": HTMLTest() HTMLTextTest() TemplateTest() if _c_htmltext: CHTMLTest() CHTMLTextTest() CTemplateTest() Quixote-2.7b2/quixote/html/__init__.py0000664000175000017500000000762711114154100016145 0ustar nasnas"""Various functions for dealing with HTML. These functions are fairly simple but it is critical that they be used correctly. Many security problems are caused by escaping errors (cross site scripting is one example). The HTML and XML standards on www.w3c.org and www.xml.com should be studied, especially the sections on character sets, entities, attribute and values. htmltext and htmlescape ----------------------- This type and function are meant to be used with [html] PTL template type. The htmltext type designates data that does not need to be escaped and the htmlescape() function calls str() on the argment, escapes the resulting string and returns a htmltext instance. htmlescape() does nothing to htmltext instances. url_quote --------- Use for quoting data to be included as part of a URL, for example: input = "foo bar" ... '' % url_quote(input) Note that URLs are usually used as attribute values and might need to have HTML special characters escaped. As an example of incorrect usage: url = 'http://example.com/?a=1©=0' # INCORRECT url = 'http://example.com/?a=1&copy=0' # CORRECT ... 'do something' % url Old browsers would treat "©" as an entity reference and replace it with the copyright character. XML processors should treat it as an invalid entity reference. """ import urllib try: # faster C implementation from quixote.html._c_htmltext import htmltext, htmlescape, \ stringify, TemplateIO except ImportError: from quixote.html._py_htmltext import htmltext, htmlescape, \ stringify, TemplateIO ValuelessAttr = object() # magic singleton object def htmltag(tag, xml_end=False, css_class=None, **attrs): """Create a HTML tag. """ r = ["<%s" % tag] if css_class is not None: attrs['class'] = css_class for (attr, val) in attrs.items(): if val is ValuelessAttr: val = attr if val is not None: r.append(' %s="%s"' % (attr, stringify(htmlescape(val)))) if xml_end: r.append(" />") else: r.append(">") return htmltext("".join(r)) def href(url, text, title=None, **attrs): return (htmltag("a", href=url, title=title, **attrs) + htmlescape(text) + htmltext("")) def url_with_query(path, **attrs): result = htmltext(url_quote(path)) if attrs: result += "?" + "&".join([url_quote(key) + "=" + url_quote(value) for key, value in attrs.items()]) return result def nl2br(value): """nl2br(value : any) -> htmltext Insert
tags before newline characters. """ text = htmlescape(value) return htmltext(text.s.replace('\n', '
\n')) def url_quote(value, fallback=None): """url_quote(value : any [, fallback : string]) -> string Quotes 'value' for use in a URL; see urllib.quote(). If value is None, then the behavior depends on the fallback argument. If it is not supplied then an error is raised. Otherwise, the fallback value is returned unquoted. """ if value is None: if fallback is None: raise ValueError, "value is None and no fallback supplied" else: return fallback return urllib.quote(stringify(value)) _saved = None def use_qpy(): """ Switch to using 'qpy' as an alternative. """ import qpy from qpy_templateio import qpy_TemplateIO global _saved, htmltext, stringify, htmlescape, TemplateIO if not _saved: _saved = (htmltext, stringify, htmlescape, TemplateIO) htmltext = qpy.h8 stringify = qpy.stringify htmlescape = qpy.h8.quote TemplateIO = qpy_TemplateIO def cleanup_qpy(): global _saved, htmltext, stringify, htmlescape, TemplateIO (htmltext, stringify, htmlescape, TemplateIO) = _saved _saved = None Quixote-2.7b2/quixote/html/_c_htmltext.c0000664000175000017500000005703611114154100016511 0ustar nasnas/* htmltext type and the htmlescape function */ #include "Python.h" #include "structmember.h" #if PY_VERSION_HEX < 0x02050000 typedef int Py_ssize_t; typedef intargfunc ssizeargfunc; typedef inquiry lenfunc; #endif typedef struct { PyObject_HEAD PyObject *s; } htmltextObject; static PyTypeObject htmltext_Type; #define htmltextObject_Check(v) PyType_IsSubtype((v)->ob_type, &htmltext_Type) #define htmltext_STR(v) ((PyObject *)(((htmltextObject *)v)->s)) typedef struct { PyObject_HEAD PyObject *obj; } QuoteWrapperObject; static PyTypeObject QuoteWrapper_Type; #define QuoteWrapper_Check(v) ((v)->ob_type == &QuoteWrapper_Type) typedef struct { PyUnicodeObject escaped; PyObject *raw; } UnicodeWrapperObject; static PyTypeObject UnicodeWrapper_Type; #define UnicodeWrapper_Check(v) ((v)->ob_type == &UnicodeWrapper_Type) typedef struct { PyObject_HEAD PyObject *data; /* PyList_Object */ int html; } TemplateIO_Object; static PyTypeObject TemplateIO_Type; #define TemplateIO_Check(v) ((v)->ob_type == &TemplateIO_Type) static PyObject * type_error(const char *msg) { PyErr_SetString(PyExc_TypeError, msg); return NULL; } static int string_check(PyObject *v) { return PyUnicode_Check(v) || PyString_Check(v); } static PyObject * stringify(PyObject *obj) { static PyObject *unicodestr = NULL; PyObject *res, *func; if (string_check(obj)) { Py_INCREF(obj); return obj; } if (unicodestr == NULL) { unicodestr = PyString_InternFromString("__unicode__"); if (unicodestr == NULL) return NULL; } func = PyObject_GetAttr(obj, unicodestr); if (func != NULL) { res = PyEval_CallObject(func, (PyObject *)NULL); Py_DECREF(func); } else { PyErr_Clear(); if (obj->ob_type->tp_str != NULL) res = (*obj->ob_type->tp_str)(obj); else res = PyObject_Repr(obj); } if (res == NULL) return NULL; if (!string_check(res)) { Py_DECREF(res); return type_error("string object required"); } return res; } static PyObject * escape_string(PyObject *obj) { char *s; PyObject *newobj; Py_ssize_t i, j, extra_space, size, new_size; assert (PyString_Check(obj)); size = PyString_GET_SIZE(obj); extra_space = 0; for (i=0; i < size; i++) { switch (PyString_AS_STRING(obj)[i]) { case '&': extra_space += 4; break; case '<': case '>': extra_space += 3; break; case '"': extra_space += 5; break; } } if (extra_space == 0) { Py_INCREF(obj); return (PyObject *)obj; } new_size = size + extra_space; newobj = PyString_FromStringAndSize(NULL, new_size); if (newobj == NULL) return NULL; s = PyString_AS_STRING(newobj); for (i=0, j=0; i < size; i++) { switch (PyString_AS_STRING(obj)[i]) { case '&': s[j++] = '&'; s[j++] = 'a'; s[j++] = 'm'; s[j++] = 'p'; s[j++] = ';'; break; case '<': s[j++] = '&'; s[j++] = 'l'; s[j++] = 't'; s[j++] = ';'; break; case '>': s[j++] = '&'; s[j++] = 'g'; s[j++] = 't'; s[j++] = ';'; break; case '"': s[j++] = '&'; s[j++] = 'q'; s[j++] = 'u'; s[j++] = 'o'; s[j++] = 't'; s[j++] = ';'; break; default: s[j++] = PyString_AS_STRING(obj)[i]; break; } } assert (j == new_size); return (PyObject *)newobj; } static PyObject * escape_unicode(PyObject *obj) { Py_UNICODE *u; PyObject *newobj; Py_ssize_t i, j, extra_space, size, new_size; assert (PyUnicode_Check(obj)); size = PyUnicode_GET_SIZE(obj); extra_space = 0; for (i=0; i < size; i++) { switch (PyUnicode_AS_UNICODE(obj)[i]) { case '&': extra_space += 4; break; case '<': case '>': extra_space += 3; break; case '"': extra_space += 5; break; } } if (extra_space == 0) { Py_INCREF(obj); return (PyObject *)obj; } new_size = size + extra_space; newobj = PyUnicode_FromUnicode(NULL, new_size); if (newobj == NULL) { return NULL; } u = PyUnicode_AS_UNICODE(newobj); for (i=0, j=0; i < size; i++) { switch (PyUnicode_AS_UNICODE(obj)[i]) { case '&': u[j++] = '&'; u[j++] = 'a'; u[j++] = 'm'; u[j++] = 'p'; u[j++] = ';'; break; case '<': u[j++] = '&'; u[j++] = 'l'; u[j++] = 't'; u[j++] = ';'; break; case '>': u[j++] = '&'; u[j++] = 'g'; u[j++] = 't'; u[j++] = ';'; break; case '"': u[j++] = '&'; u[j++] = 'q'; u[j++] = 'u'; u[j++] = 'o'; u[j++] = 't'; u[j++] = ';'; break; default: u[j++] = PyUnicode_AS_UNICODE(obj)[i]; break; } } assert (j == new_size); return (PyObject *)newobj; } static PyObject * escape(PyObject *obj) { if (PyString_Check(obj)) { return escape_string(obj); } else if (PyUnicode_Check(obj)) { return escape_unicode(obj); } else { return type_error("string object required"); } } static PyObject * quote_wrapper_new(PyObject *o) { QuoteWrapperObject *self; if (htmltextObject_Check(o)) { /* Necessary to work around a PyString_Format bug. Should be * fixed in Python 2.5. */ o = htmltext_STR(o); Py_INCREF(o); return o; } if (PyUnicode_Check(o)) { /* again, work around PyString_Format bug */ return PyObject_CallFunctionObjArgs( (PyObject *)&UnicodeWrapper_Type, o, NULL); } if (PyInt_Check(o) || PyFloat_Check(o) || PyLong_Check(o)) { /* no need for wrapper */ Py_INCREF(o); return o; } self = PyObject_New(QuoteWrapperObject, &QuoteWrapper_Type); if (self == NULL) return NULL; Py_INCREF(o); self->obj = o; return (PyObject *)self; } static void quote_wrapper_dealloc(QuoteWrapperObject *self) { Py_DECREF(self->obj); PyObject_Del(self); } static PyObject * quote_wrapper_repr(QuoteWrapperObject *self) { PyObject *qs; PyObject *s = PyObject_Repr(self->obj); if (s == NULL) return NULL; qs = escape(s); Py_DECREF(s); return qs; } static PyObject * quote_wrapper_str(QuoteWrapperObject *self) { PyObject *qs; PyObject *s = stringify(self->obj); if (s == NULL) return NULL; qs = escape(s); Py_DECREF(s); return qs; } static PyObject * quote_wrapper_subscript(QuoteWrapperObject *self, PyObject *key) { PyObject *v, *w;; v = PyObject_GetItem(self->obj, key); if (v == NULL) { return NULL; } w = quote_wrapper_new(v); Py_DECREF(v); return w; } static PyObject * unicode_wrapper_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyObject *result; PyObject *raw = NULL, *escaped = NULL, *newargs = NULL; if (!PyArg_ParseTuple(args, "O", &raw)) goto error; escaped = escape(raw); if (escaped == NULL) goto error; newargs = PyTuple_New(1); if (newargs == NULL) goto error; PyTuple_SET_ITEM(newargs, 0, escaped); result = PyUnicode_Type.tp_new(type, newargs, kwds); if (result == NULL) goto error; Py_DECREF(newargs); Py_INCREF(raw); ((UnicodeWrapperObject *)result)->raw = raw; return result; error: Py_XDECREF(raw); Py_XDECREF(escaped); Py_XDECREF(newargs); return NULL; } static void unicode_wrapper_dealloc(UnicodeWrapperObject *self) { Py_XDECREF(self->raw); PyUnicode_Type.tp_dealloc((PyObject *) self); } static PyObject * unicode_wrapper_repr(UnicodeWrapperObject *self) { PyObject *qr; PyObject *r = PyObject_Repr(self->raw); if (r == NULL) return NULL; qr = escape(r); Py_DECREF(r); return qr; } static PyObject * htmltext_from_string(PyObject *s) { /* note, this steals a reference */ PyObject *self; if (s == NULL) return NULL; assert (string_check(s)); self = PyType_GenericAlloc(&htmltext_Type, 0); if (self == NULL) { Py_DECREF(s); return NULL; } ((htmltextObject *)self)->s = s; return self; } static PyObject * htmltext_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { htmltextObject *self; PyObject *s; static char *kwlist[] = {"s", 0}; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:htmltext", kwlist, &s)) return NULL; s = stringify(s); if (s == NULL) return NULL; self = (htmltextObject *)type->tp_alloc(type, 0); if (self == NULL) { Py_DECREF(s); return NULL; } self->s = s; return (PyObject *)self; } /* htmltext methods */ static void htmltext_dealloc(htmltextObject *self) { Py_DECREF(self->s); self->ob_type->tp_free((PyObject *)self); } static long htmltext_hash(PyObject *self) { return PyObject_Hash(htmltext_STR(self)); } static PyObject * htmltext_str(htmltextObject *self) { Py_INCREF(self->s); return (PyObject *)self->s; } static PyObject * htmltext_repr(htmltextObject *self) { PyObject *sr, *rv; sr = PyObject_Repr((PyObject *)self->s); if (sr == NULL) return NULL; rv = PyString_FromFormat("", PyString_AsString(sr)); Py_DECREF(sr); return rv; } static PyObject * htmltext_richcompare(PyObject *a, PyObject *b, int op) { if (htmltextObject_Check(a)) { a = htmltext_STR(a); } if (htmltextObject_Check(b)) { b = htmltext_STR(b); } return PyObject_RichCompare(a, b, op); } static Py_ssize_t htmltext_length(htmltextObject *self) { return PyObject_Size(htmltext_STR(self)); } static PyObject * htmltext_format(htmltextObject *self, PyObject *args) { /* wrap the format arguments with QuoteWrapperObject */ int is_unicode; PyObject *rv, *wargs; if (PyUnicode_Check(self->s)) { is_unicode = 1; } else { is_unicode = 0; assert (PyString_Check(self->s)); } if (PyTuple_Check(args)) { Py_ssize_t i, n = PyTuple_GET_SIZE(args); wargs = PyTuple_New(n); for (i=0; i < n; i++) { PyObject *v = PyTuple_GET_ITEM(args, i); v = quote_wrapper_new(v); if (v == NULL) { Py_DECREF(wargs); return NULL; } PyTuple_SetItem(wargs, i, v); } } else { wargs = quote_wrapper_new(args); if (wargs == NULL) return NULL; } if (is_unicode) rv = PyUnicode_Format(self->s, wargs); else rv = PyString_Format(self->s, wargs); Py_DECREF(wargs); return htmltext_from_string(rv); } static PyObject * htmltext_add(PyObject *v, PyObject *w) { PyObject *qv, *qw, *rv; if (htmltextObject_Check(v) && htmltextObject_Check(w)) { qv = htmltext_STR(v); qw = htmltext_STR(w); Py_INCREF(qv); Py_INCREF(qw); } else if (string_check(w)) { assert (htmltextObject_Check(v)); qv = htmltext_STR(v); qw = escape(w); if (qw == NULL) return NULL; Py_INCREF(qv); } else if (string_check(v)) { assert (htmltextObject_Check(w)); qv = escape(v); if (qv == NULL) return NULL; qw = htmltext_STR(w); Py_INCREF(qw); } else { Py_INCREF(Py_NotImplemented); return Py_NotImplemented; } if (PyString_Check(qv)) { PyString_ConcatAndDel(&qv, qw); rv = qv; } else { assert (PyUnicode_Check(qv)); rv = PyUnicode_Concat(qv, qw); Py_DECREF(qv); Py_DECREF(qw); } return htmltext_from_string(rv); } static PyObject * htmltext_repeat(htmltextObject *self, Py_ssize_t n) { PyObject *s = PySequence_Repeat(htmltext_STR(self), n); if (s == NULL) return NULL; return htmltext_from_string(s); } static PyObject * htmltext_join(PyObject *self, PyObject *args) { Py_ssize_t i; PyObject *quoted_args, *rv; quoted_args = PySequence_List(args); if (quoted_args == NULL) return NULL; for (i=0; i < PyList_Size(quoted_args); i++) { PyObject *value, *qvalue; value = PyList_GET_ITEM(quoted_args, i); if (value == NULL) { goto error; } if (htmltextObject_Check(value)) { qvalue = htmltext_STR(value); Py_INCREF(qvalue); } else { if (!string_check(value)) { type_error("join requires a list of strings"); goto error; } qvalue = escape(value); if (qvalue == NULL) goto error; } if (PyList_SetItem(quoted_args, i, qvalue) < 0) { goto error; } } if (PyUnicode_Check(htmltext_STR(self))) { rv = PyUnicode_Join(htmltext_STR(self), quoted_args); } else { rv = _PyString_Join(htmltext_STR(self), quoted_args); } Py_DECREF(quoted_args); return htmltext_from_string(rv); error: Py_DECREF(quoted_args); return NULL; } static PyObject * quote_arg(PyObject *s) { PyObject *ss; if (string_check(s)) { ss = escape(s); if (ss == NULL) return NULL; } else if (htmltextObject_Check(s)) { ss = htmltext_STR(s); Py_INCREF(ss); } else { return type_error("string object required"); } return ss; } static PyObject * htmltext_call_method1(PyObject *self, PyObject *s, char *method) { PyObject *ss, *rv; ss = quote_arg(s); if (ss == NULL) return NULL; rv = PyObject_CallMethod(htmltext_STR(self), method, "O", ss); Py_DECREF(ss); return rv; } static PyObject * htmltext_startswith(PyObject *self, PyObject *s) { return htmltext_call_method1(self, s, "startswith"); } static PyObject * htmltext_endswith(PyObject *self, PyObject *s) { return htmltext_call_method1(self, s, "endswith"); } static PyObject * htmltext_replace(PyObject *self, PyObject *args) { PyObject *old, *new, *q_old, *q_new, *rv; Py_ssize_t maxsplit = -1; #if PY_HEX_VERSION >= 0x02050000 if (!PyArg_ParseTuple(args,"OO|n:replace", &old, &new, &maxsplit)) return NULL; #else if (!PyArg_ParseTuple(args,"OO|i:replace", &old, &new, &maxsplit)) return NULL; #endif q_old = quote_arg(old); if (q_old == NULL) return NULL; q_new = quote_arg(new); if (q_new == NULL) { Py_DECREF(q_old); return NULL; } #if PY_HEX_VERSION >= 0x02050000 rv = PyObject_CallMethod(htmltext_STR(self), "replace", "OOn", q_old, q_new, maxsplit); #else rv = PyObject_CallMethod(htmltext_STR(self), "replace", "OOi", q_old, q_new, maxsplit); #endif Py_DECREF(q_old); Py_DECREF(q_new); return htmltext_from_string(rv); } static PyObject * htmltext_lower(PyObject *self) { return htmltext_from_string(PyObject_CallMethod(htmltext_STR(self), "lower", "")); } static PyObject * htmltext_upper(PyObject *self) { return htmltext_from_string(PyObject_CallMethod(htmltext_STR(self), "upper", "")); } static PyObject * htmltext_capitalize(PyObject *self) { return htmltext_from_string(PyObject_CallMethod(htmltext_STR(self), "capitalize", "")); } static PyObject * template_io_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { TemplateIO_Object *self; int html = 0; static char *kwlist[] = {"html", 0}; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i:TemplateIO", kwlist, &html)) return NULL; self = (TemplateIO_Object *)type->tp_alloc(type, 0); if (self == NULL) { return NULL; } self->data = PyList_New(0); if (self->data == NULL) { Py_DECREF(self); return NULL; } self->html = html != 0; return (PyObject *)self; } static void template_io_dealloc(TemplateIO_Object *self) { Py_DECREF(self->data); self->ob_type->tp_free((PyObject *)self); } static PyObject * template_io_str(TemplateIO_Object *self) { static PyObject *empty = NULL; if (empty == NULL) { empty = PyString_FromStringAndSize(NULL, 0); if (empty == NULL) return NULL; } return _PyString_Join(empty, self->data); } static PyObject * template_io_getvalue(TemplateIO_Object *self) { if (self->html) { return htmltext_from_string(template_io_str(self)); } else { return template_io_str(self); } } static PyObject * template_io_iadd(TemplateIO_Object *self, PyObject *other) { PyObject *s = NULL; if (!TemplateIO_Check(self)) return type_error("TemplateIO object required"); if (other == Py_None) { Py_INCREF(self); return (PyObject *)self; } else if (htmltextObject_Check(other)) { s = htmltext_STR(other); Py_INCREF(s); } else { if (self->html) { PyObject *ss = stringify(other); if (ss == NULL) return NULL; s = escape(ss); Py_DECREF(ss); } else { s = stringify(other); } if (s == NULL) return NULL; } if (PyList_Append(self->data, s) != 0) return NULL; Py_DECREF(s); Py_INCREF(self); return (PyObject *)self; } static PyMethodDef htmltext_methods[] = { {"join", (PyCFunction)htmltext_join, METH_O, ""}, {"startswith", (PyCFunction)htmltext_startswith, METH_O, ""}, {"endswith", (PyCFunction)htmltext_endswith, METH_O, ""}, {"replace", (PyCFunction)htmltext_replace, METH_VARARGS, ""}, {"lower", (PyCFunction)htmltext_lower, METH_NOARGS, ""}, {"upper", (PyCFunction)htmltext_upper, METH_NOARGS, ""}, {"capitalize", (PyCFunction)htmltext_capitalize, METH_NOARGS, ""}, {NULL, NULL} }; static PyMemberDef htmltext_members[] = { {"s", T_OBJECT, offsetof(htmltextObject, s), READONLY, "the string"}, {NULL}, }; static PySequenceMethods htmltext_as_sequence = { (lenfunc)htmltext_length, /*sq_length*/ 0, /*sq_concat*/ (ssizeargfunc)htmltext_repeat, /*sq_repeat*/ 0, /*sq_item*/ 0, /*sq_slice*/ 0, /*sq_ass_item*/ 0, /*sq_ass_slice*/ 0, /*sq_contains*/ }; static PyNumberMethods htmltext_as_number = { (binaryfunc)htmltext_add, /*nb_add*/ 0, /*nb_subtract*/ 0, /*nb_multiply*/ 0, /*nb_divide*/ (binaryfunc)htmltext_format, /*nb_remainder*/ 0, /*nb_divmod*/ 0, /*nb_power*/ 0, /*nb_negative*/ 0, /*nb_positive*/ 0, /*nb_absolute*/ 0, /*nb_nonzero*/ 0, /*nb_invert*/ 0, /*nb_lshift*/ 0, /*nb_rshift*/ 0, /*nb_and*/ 0, /*nb_xor*/ 0, /*nb_or*/ 0, /*nb_coerce*/ 0, /*nb_int*/ 0, /*nb_long*/ 0, /*nb_float*/ }; static PyTypeObject htmltext_Type = { PyObject_HEAD_INIT(NULL) 0, /*ob_size*/ "htmltext", /*tp_name*/ sizeof(htmltextObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)htmltext_dealloc, /*tp_dealloc*/ 0, /*tp_print*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ 0, /*tp_compare*/ (unaryfunc)htmltext_repr,/*tp_repr*/ &htmltext_as_number, /*tp_as_number*/ &htmltext_as_sequence, /*tp_as_sequence*/ 0, /*tp_as_mapping*/ htmltext_hash, /*tp_hash*/ 0, /*tp_call*/ (unaryfunc)htmltext_str,/*tp_str*/ 0, /*tp_getattro set to PyObject_GenericGetAttr by module init*/ 0, /*tp_setattro*/ 0, /*tp_as_buffer*/ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE \ | Py_TPFLAGS_CHECKTYPES, /*tp_flags*/ 0, /*tp_doc*/ 0, /*tp_traverse*/ 0, /*tp_clear*/ htmltext_richcompare, /*tp_richcompare*/ 0, /*tp_weaklistoffset*/ 0, /*tp_iter*/ 0, /*tp_iternext*/ htmltext_methods, /*tp_methods*/ htmltext_members, /*tp_members*/ 0, /*tp_getset*/ 0, /*tp_base*/ 0, /*tp_dict*/ 0, /*tp_descr_get*/ 0, /*tp_descr_set*/ 0, /*tp_dictoffset*/ 0, /*tp_init*/ 0, /*tp_alloc set to PyType_GenericAlloc by module init*/ htmltext_new, /*tp_new*/ 0, /*tp_free set to _PyObject_Del by module init*/ 0, /*tp_is_gc*/ }; static PyNumberMethods quote_wrapper_as_number = { 0, /*nb_add*/ 0, /*nb_subtract*/ 0, /*nb_multiply*/ 0, /*nb_divide*/ 0, /*nb_remainder*/ 0, /*nb_divmod*/ 0, /*nb_power*/ 0, /*nb_negative*/ 0, /*nb_positive*/ 0, /*nb_absolute*/ 0, /*nb_nonzero*/ 0, /*nb_invert*/ 0, /*nb_lshift*/ 0, /*nb_rshift*/ 0, /*nb_and*/ 0, /*nb_xor*/ 0, /*nb_or*/ 0, /*nb_coerce*/ 0, /*nb_int*/ 0, /*nb_long*/ 0, /*nb_float*/ }; static PyMappingMethods quote_wrapper_as_mapping = { 0, /*mp_length*/ (binaryfunc)quote_wrapper_subscript, /*mp_subscript*/ 0, /*mp_ass_subscript*/ }; static PyTypeObject QuoteWrapper_Type = { PyObject_HEAD_INIT(NULL) 0, /*ob_size*/ "QuoteWrapper", /*tp_name*/ sizeof(QuoteWrapperObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)quote_wrapper_dealloc, /*tp_dealloc*/ 0, /*tp_print*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ 0, /*tp_compare*/ (unaryfunc)quote_wrapper_repr,/*tp_repr*/ "e_wrapper_as_number,/*tp_as_number*/ 0, /*tp_as_sequence*/ "e_wrapper_as_mapping,/*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ (unaryfunc)quote_wrapper_str, /*tp_str*/ }; static PyTypeObject UnicodeWrapper_Type = { PyObject_HEAD_INIT(NULL) 0, /*ob_size*/ "UnicodeWrapper", /*tp_name*/ sizeof(UnicodeWrapperObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)unicode_wrapper_dealloc, /*tp_dealloc*/ 0, /*tp_print*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ 0, /*tp_compare*/ (unaryfunc)unicode_wrapper_repr, /*tp_repr*/ 0, /*tp_as_number*/ 0, /*tp_as_sequence*/ 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ 0, /*tp_getattro */ 0, /*tp_setattro */ 0, /*tp_as_buffer */ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE, /*tp_flags */ 0, /*tp_doc */ 0, /*tp_traverse */ 0, /*tp_clear */ 0, /*tp_richcompare */ 0, /*tp_weaklistoffset */ 0, /*tp_iter */ 0, /*tp_iternext */ 0, /*tp_methods */ 0, /*tp_members */ 0, /*tp_getset */ 0, /*tp_base */ 0, /*tp_dict */ 0, /*tp_descr_get */ 0, /*tp_descr_set */ 0, /*tp_dictoffset */ 0, /*tp_init */ 0, /*tp_alloc */ (newfunc)unicode_wrapper_new, /*tp_new */ }; static PyNumberMethods template_io_as_number = { 0, /*nb_add*/ 0, /*nb_subtract*/ 0, /*nb_multiply*/ 0, /*nb_divide*/ 0, /*nb_remainder*/ 0, /*nb_divmod*/ 0, /*nb_power*/ 0, /*nb_negative*/ 0, /*nb_positive*/ 0, /*nb_absolute*/ 0, /*nb_nonzero*/ 0, /*nb_invert*/ 0, /*nb_lshift*/ 0, /*nb_rshift*/ 0, /*nb_and*/ 0, /*nb_xor*/ 0, /*nb_or*/ 0, /*nb_coerce*/ 0, /*nb_int*/ 0, /*nb_long*/ 0, /*nb_float*/ 0, /*nb_oct*/ 0, /*nb_hex*/ (binaryfunc)template_io_iadd, /*nb_inplace_add*/ }; static PyMethodDef template_io_methods[] = { {"getvalue", (PyCFunction)template_io_getvalue, METH_NOARGS, ""}, {NULL, NULL} }; static PyTypeObject TemplateIO_Type = { PyObject_HEAD_INIT(NULL) 0, /*ob_size*/ "TemplateIO", /*tp_name*/ sizeof(TemplateIO_Object),/*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)template_io_dealloc, /*tp_dealloc*/ 0, /*tp_print*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ 0, /*tp_compare*/ 0, /*tp_repr*/ &template_io_as_number, /*tp_as_number*/ 0, /*tp_as_sequence*/ 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ (unaryfunc)template_io_str,/*tp_str*/ 0, /*tp_getattro set to PyObject_GenericGetAttr by module init*/ 0, /*tp_setattro*/ 0, /*tp_as_buffer*/ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ 0, /*tp_doc*/ 0, /*tp_traverse*/ 0, /*tp_clear*/ 0, /*tp_richcompare*/ 0, /*tp_weaklistoffset*/ 0, /*tp_iter*/ 0, /*tp_iternext*/ template_io_methods, /*tp_methods*/ 0, /*tp_members*/ 0, /*tp_getset*/ 0, /*tp_base*/ 0, /*tp_dict*/ 0, /*tp_descr_get*/ 0, /*tp_descr_set*/ 0, /*tp_dictoffset*/ 0, /*tp_init*/ 0, /*tp_alloc set to PyType_GenericAlloc by module init*/ template_io_new, /*tp_new*/ 0, /*tp_free set to _PyObject_Del by module init*/ 0, /*tp_is_gc*/ }; /* --------------------------------------------------------------------- */ static PyObject * html_escape(PyObject *self, PyObject *o) { if (htmltextObject_Check(o)) { Py_INCREF(o); return o; } else { PyObject *rv; PyObject *s = stringify(o); if (s == NULL) return NULL; rv = escape(s); Py_DECREF(s); return htmltext_from_string(rv); } } static PyObject * py_escape_string(PyObject *self, PyObject *o) { return escape(o); } static PyObject * py_stringify(PyObject *self, PyObject *o) { return stringify(o); } /* List of functions defined in the module */ static PyMethodDef htmltext_module_methods[] = { {"htmlescape", (PyCFunction)html_escape, METH_O}, {"_escape_string", (PyCFunction)py_escape_string, METH_O}, {"stringify", (PyCFunction)py_stringify, METH_O}, {NULL, NULL} }; static char module_doc[] = "htmltext string type"; void init_c_htmltext(void) { PyObject *m; /* Create the module and add the functions */ m = Py_InitModule4("_c_htmltext", htmltext_module_methods, module_doc, NULL, PYTHON_API_VERSION); if (PyType_Ready(&htmltext_Type) < 0) return; if (PyType_Ready(&QuoteWrapper_Type) < 0) return; UnicodeWrapper_Type.tp_base = &PyUnicode_Type; if (PyType_Ready(&UnicodeWrapper_Type) < 0) return; if (PyType_Ready(&TemplateIO_Type) < 0) return; Py_INCREF((PyObject *)&htmltext_Type); Py_INCREF((PyObject *)&QuoteWrapper_Type); Py_INCREF((PyObject *)&UnicodeWrapper_Type); Py_INCREF((PyObject *)&TemplateIO_Type); PyModule_AddObject(m, "htmltext", (PyObject *)&htmltext_Type); PyModule_AddObject(m, "TemplateIO", (PyObject *)&TemplateIO_Type); } Quixote-2.7b2/quixote/html/_py_htmltext.py0000664000175000017500000001403611114154100017116 0ustar nasnas"""Python implementation of the htmltext type, the htmlescape function and TemplateIO. """ def _escape_string(s): if not isinstance(s, basestring): raise TypeError, 'string object required' s = s.replace("&", "&") s = s.replace("<", "<") s = s.replace(">", ">") s = s.replace('"', """) return s def stringify(obj): """Return 'obj' as a string or unicode object. Tries to prevent turning strings into unicode objects. """ tp = type(obj) if issubclass(tp, basestring): return obj elif hasattr(tp, '__unicode__'): s = tp.__unicode__(obj) if not isinstance(s, basestring): raise TypeError, '__unicode__ did not return a string' return s elif hasattr(tp, '__str__'): s = tp.__str__(obj) if not isinstance(s, basestring): raise TypeError, '__str__ did not return a string' return s else: return str(obj) class htmltext(object): """The htmltext string-like type. This type serves as a tag signifying that HTML special characters do not need to be escaped using entities. """ __slots__ = ['s'] def __init__(self, s): self.s = stringify(s) # XXX make read-only #def __setattr__(self, name, value): # raise AttributeError, 'immutable object' def __getstate__(self): raise ValueError, 'htmltext objects should not be pickled' def __repr__(self): return '' % self.s def __str__(self): return self.s def __len__(self): return len(self.s) def __cmp__(self, other): return cmp(self.s, other) def __hash__(self): return hash(self.s) def __mod__(self, args): if isinstance(args, tuple): return htmltext(self.s % tuple(map(_wraparg, args))) else: return htmltext(self.s % _wraparg(args)) def __add__(self, other): if isinstance(other, basestring): return htmltext(self.s + _escape_string(other)) elif isinstance(other, htmltext): return htmltext(self.s + other.s) else: return NotImplemented def __radd__(self, other): if isinstance(other, basestring): return htmltext(_escape_string(other) + self.s) else: return NotImplemented def __mul__(self, n): return htmltext(self.s * n) def join(self, items): quoted_items = [] for item in items: if isinstance(item, htmltext): quoted_items.append(stringify(item)) elif isinstance(item, basestring): quoted_items.append(_escape_string(item)) else: raise TypeError( 'join() requires string arguments (got %r)' % item) return htmltext(self.s.join(quoted_items)) def startswith(self, s): if isinstance(s, htmltext): s = s.s else: s = _escape_string(s) return self.s.startswith(s) def endswith(self, s): if isinstance(s, htmltext): s = s.s else: s = _escape_string(s) return self.s.endswith(s) def replace(self, old, new, count=-1): if isinstance(old, htmltext): old = old.s else: old = _escape_string(old) if isinstance(new, htmltext): new = new.s else: new = _escape_string(new) return htmltext(self.s.replace(old, new, count)) def lower(self): return htmltext(self.s.lower()) def upper(self): return htmltext(self.s.upper()) def capitalize(self): return htmltext(self.s.capitalize()) class _QuoteWrapper(object): # helper for htmltext class __mod__ __slots__ = ['value'] def __init__(self, value): self.value = value def __str__(self): return _escape_string(stringify(self.value)) def __repr__(self): return _escape_string(`self.value`) def __getitem__(self, key): return _wraparg(self.value[key]) class _UnicodeWrapper(unicode): __slots__ = ['raw'] def __new__(cls, s): result = unicode.__new__(cls, _escape_string(s)) result.raw = s return result def __repr__(self): return _escape_string(`self.raw`) def _wraparg(arg): if isinstance(arg, htmltext): # necessary to work around a PyString_Format bug in Python. Should # be fixed in Python 2.5 return stringify(arg) elif isinstance(arg, unicode): # again, work around PyString_Format bug return _UnicodeWrapper(arg) elif (isinstance(arg, int) or isinstance(arg, long) or isinstance(arg, float)): # ints, longs, floats are okay return arg else: # everything is gets wrapped return _QuoteWrapper(arg) def htmlescape(s): """htmlescape(s) -> htmltext Return an 'htmltext' object using the argument. If the argument is not already a 'htmltext' object then the HTML markup characters \", <, >, and & are first escaped. """ if isinstance(s, htmltext): return s else: s = stringify(s) # inline _escape_string for speed s = s.replace("&", "&") # must be done first s = s.replace("<", "<") s = s.replace(">", ">") s = s.replace('"', """) return htmltext(s) class TemplateIO(object): """Collect output for PTL scripts. """ __slots__ = ['html', 'data'] def __init__(self, html=False): self.html = html self.data = [] def __iadd__(self, other): if other is not None: self.data.append(other) return self def __repr__(self): return ("<%s at %x: %d chunks>" % (self.__class__.__name__, id(self), len(self.data))) def __str__(self): return stringify(self.getvalue()) def getvalue(self): if self.html: return htmltext('').join(map(htmlescape, self.data)) else: return ''.join(map(stringify, self.data)) Quixote-2.7b2/quixote/html/qpy_templateio.py0000664000175000017500000000200511114154100017423 0ustar nasnastry: import qpy class qpy_TemplateIO(object): # The only difference between this and quixote.html.TemplateIO is # the .getvalue() method and the location of stringify. # We redefine the class from scratch only because a subclass can't inherit # .__iadd__ apparently: you get a TypeError that the class is not the # parent type. __slots__ = ['html', 'data'] def __init__(self, html=False): self.html = html self.data = [] def __iadd__(self, other): if other is not None: self.data.append(other) return self def __repr__(self): return ("<%s at %x: %d chunks>" % (self.__class__.__name__, id(self), len(self.data))) def __str__(self): return qpy.stringify(self.getvalue()) def getvalue(self): klass = self.html and qpy.h8 or qpy.u8 return klass.from_list(self.data) except ImportError: pass Quixote-2.7b2/quixote/ptl/0000775000175000017500000000000011326377244013700 5ustar nasnasQuixote-2.7b2/quixote/ptl/test/0000775000175000017500000000000011326377244014657 5ustar nasnasQuixote-2.7b2/quixote/ptl/test/utest_ptl.py0000775000175000017500000000340411326376506017260 0ustar nasnas#!/usr/bin/env python from sancho.utest import UTest from quixote.ptl.ptl_compile import compile_template from StringIO import StringIO from quixote.html import TemplateIO, htmltext def run_ptl(*source): """ Compile the given lines of source code using the ptl compiler and run the resulting compiled code. """ # When the ptl compiler compiles a module, it places _q_TemplateIO # and _q_htmltext into the globals of the module. Here, we don't # have a module, but we provide these same globals for eval. eval(compile_template(StringIO('\n'.join(source)), 'test'), dict(_q_TemplateIO=TemplateIO, _q_htmltext=htmltext)) class Test (UTest): def check_html(self): run_ptl( 'from quixote.html import htmltext', 'def f [html] (a):', ' "&"', ' a', 'assert type(f(1)) == htmltext', 'assert f("") == "&"', 'assert f("&") == "&&"', 'assert f(htmltext("&")) == "&&"') def check_plain(self): run_ptl( 'from quixote.html import htmltext', 'def f [plain] (a):', ' "&"', ' a', 'assert type(f(1)) == str', 'assert f("") == "&"', 'assert f("&") == "&&"', 'assert f(htmltext("&")) == "&&"', 'assert type(f(htmltext("&"))) == str') def check_syntax(self): run_ptl('def f(a):\n a') try: run_ptl('def f [] (a):\n a') assert 0 except SyntaxError, e: assert e.lineno == 1 try: run_ptl('def f [HTML] (a):\n a') assert 0 except SyntaxError, e: assert e.lineno == 1 if __name__ == "__main__": Test() Quixote-2.7b2/quixote/ptl/__init__.py0000664000175000017500000002426311251140226016001 0ustar nasnas''' PTL: Python Template Language ============================= Introduction ------------ PTL is the templating language used by Quixote. Most web templating languages embed a real programming language in HTML, but PTL inverts this model by merely tweaking Python to make it easier to generate HTML pages (or other forms of text). In other words, PTL is basically Python with a novel way to specify function return values. Specifically, a PTL template is designated by inserting a ``[plain]`` or ``[html]`` modifier after the function name. The value of expressions inside templates are kept, not discarded. If the type is ``[html]`` then non-literal strings are passed through a function that escapes HTML special characters. Plain text templates -------------------- Here's a sample plain text template:: def foo [plain] (x, y = 5): "This is a chunk of static text." greeting = "hello world" # statement, no PTL output print 'Input values:', x, y z = x + y """You can plug in variables like x (%s) in a variety of ways.""" % x "\n\n" "Whitespace is important in generated text.\n" "z = "; z ", but y is " y "." Obviously, templates can't have docstrings, but otherwise they follow Python's syntactic rules: indentation indicates scoping, single-quoted and triple-quoted strings can be used, the same rules for continuing lines apply, and so forth. PTL also follows all the expected semantics of normal Python code: so templates can have parameters, and the parameters can have default values, be treated as keyword arguments, etc. The difference between a template and a regular Python function is that inside a template the result of expressions are saved as the return value of that template. Look at the first part of the example again:: def foo [plain] (x, y = 5): "This is a chunk of static text." greeting = "hello world" # statement, no PTL output print 'Input values:', x, y z = x + y """You can plug in variables like x (%s) in a variety of ways.""" % x Calling this template with ``foo(1, 2)`` results in the following string:: This is a chunk of static text.You can plug in variables like x (1) in a variety of ways. Normally when Python evaluates expressions inside functions, it just discards their values, but in a ``[plain]`` PTL template the value is converted to a string using ``str()`` and appended to the template's return value. There's a single exception to this rule: ``None`` is the only value that's ever ignored, adding nothing to the output. (If this weren't the case, calling methods or functions that return ``None`` would require assigning their value to a variable. You'd have to write ``dummy = list.sort()`` in PTL code, which would be strange and confusing.) The initial string in a template isn't treated as a docstring, but is just incorporated in the generated output; therefore, templates can't have docstrings. No whitespace is ever automatically added to the output, resulting in ``...text.You can ...`` from the example. You'd have to add an extra space to one of the string literals to correct this. The assignment to the ``greeting`` local variable is a statement, not an expression, so it doesn't return a value and produces no output. The output from the ``print`` statement will be printed as usual, but won't go into the string generated by the template. Quixote directs standard output into Quixote's debugging log; if you're using PTL on its own, you should consider doing something similar. ``print`` should never be used to generate output returned to the browser, only for adding debugging traces to a template. Inside templates, you can use all of Python's control-flow statements:: def numbers [plain] (n): for i in range(n): i " " # PTL does not add any whitespace Calling ``numbers(5)`` will return the string ``"1 2 3 4 5 "``. You can also have conditional logic or exception blocks:: def international_hello [plain] (language): if language == "english": "hello" elif language == "french": "bonjour" else: raise ValueError, "I don't speak %s" % language HTML templates -------------- Since PTL is usually used to generate HTML documents, an ``[html]`` template type has been provided to make generating HTML easier. A common error when generating HTML is to grab data from the browser or from a database and incorporate the contents without escaping special characters such as '<' and '&'. This leads to a class of security bugs called "cross-site scripting" bugs, where a hostile user can insert arbitrary HTML in your site's output that can link to other sites or contain JavaScript code that does something nasty (say, popping up 10,000 browser windows). Such bugs occur because it's easy to forget to HTML-escape a string, and forgetting it in just one location is enough to open a hole. PTL offers a solution to this problem by being able to escape strings automatically when generating HTML output, at the cost of slightly diminished performance (a few percent). Here's how this feature works. PTL defines a class called ``htmltext`` that represents a string that's already been HTML-escaped and can be safely sent to the client. The function ``htmlescape(string)`` is used to escape data, and it always returns an ``htmltext`` instance. It does nothing if the argument is already ``htmltext``. If a template function is declared ``[html]`` instead of ``[text]`` then two things happen. First, all literal strings in the function become instances of ``htmltext`` instead of Python's ``str``. Second, the values of expressions are passed through ``htmlescape()`` instead of ``str()``. ``htmltext`` type is like the ``str`` type except that operations combining strings and ``htmltext`` instances will result in the string being passed through ``htmlescape()``. For example:: >>> from quixote.html import htmltext >>> htmltext('a') + 'b' >>> 'a' + htmltext('b') >>> htmltext('a%s') % 'b' >>> response = 'green eggs & ham' >>> htmltext('The response was: %s') % response Note that calling ``str()`` strips the ``htmltext`` type and should be avoided since it usually results in characters being escaped more than once. While ``htmltext`` behaves much like a regular string, it is sometimes necessary to insert a ``str()`` inside a template in order to obtain a genuine string. For example, the ``re`` module requires genuine strings. We have found that explicit calls to ``str()`` can often be avoided by splitting some code out of the template into a helper function written in regular Python. It is also recommended that the ``htmltext`` constructor be used as sparingly as possible. The reason is that when using the htmltext feature of PTL, explicit calls to ``htmltext`` become the most likely source of cross-site scripting holes. Calling ``htmltext`` is like saying "I am absolutely sure this piece of data cannot contain malicious HTML code injected by a user. Don't escape HTML special characters because I want them." Note that literal strings in template functions declared with ``[html]`` are htmltext instances, and therefore won't be escaped. You'll only need to use ``htmltext`` when HTML markup comes from outside the template. For example, if you want to include a file containing HTML:: def output_file [html] (): '' # does not get escaped htmltext(open("myfile.html").read()) '' In the common case, templates won't be dealing with HTML markup from external sources, so you can write straightforward code. Consider this function to generate the contents of the ``HEAD`` element:: def meta_tags [html] (title, description): '%s' % title '\n' % description There are no calls to ``htmlescape()`` at all, but string literals such as ``%s`` have all be turned into ``htmltext`` instances, so the string variables will be automatically escaped:: >>> t.meta_tags('Catalog', 'A catalog of our cool products') Catalog \n'> >>> t.meta_tags('Dissertation on ', ... 'Discusses the "LINK" and "META" tags') Dissertation on <HEAD> \n'> >>> Note how the title and description have had HTML-escaping applied to them. (The output has been manually pretty-printed to be more readable.) Once you start using ``htmltext`` in one of your templates, mixing plain and HTML templates is tricky because of ``htmltext``'s automatic escaping; plain templates that generate HTML tags will be double-escaped. One approach is to just use HTML templates throughout your application. Alternatively you can use ``str()`` to convert ``htmltext`` instances to regular Python strings; just be sure the resulting string isn't HTML-escaped again. Two implementations of ``htmltext`` are provided, one written in pure Python and a second one implemented as a C extension. Both versions have seen production use. PTL modules ----------- PTL templates are kept in files with the extension .ptl. Like Python files, they are byte-compiled and the byte-code is written to a compiled file with the extension ``.pyc``. Since vanilla Python doesn't know anything about PTL, this package provides an import hook. To enable it, use the following code in the __init__ module of a package containing PTL: from quixote.ptl import enable_ptl enable_ptl() Python's import mechanism is complicated and it is possible that certain combinations of packages do not work well with an import hook. An alternative mechanism is provided that does not use an import hook. To compile all the PTL files in a package, use the following code in the __init__ module: from quixote.ptl import compile_package compile_package(__path__) ''' from quixote.ptl.ptl_compile import compile_package Quixote-2.7b2/quixote/ptl/cimport.c0000664000175000017500000002531111114154100015476 0ustar nasnas/* Mostly stolen from Python/import.c. PSF license applies. */ #include "Python.h" #include "osdefs.h" #ifdef HAVE_UNISTD_H #include #endif #if PY_VERSION_HEX < 0x02050000 typedef int Py_ssize_t; #endif /* Python function to find and load a module. */ static PyObject *loader_hook; PyObject * call_find_load(char *fullname, char *subname, PyObject *path) { PyObject *args, *m; if (!(args = Py_BuildValue("(ssO)", fullname, subname, path != NULL ? path : Py_None))) return NULL; m = PyEval_CallObject(loader_hook, args); Py_DECREF(args); return m; } /* Forward declarations for helper routines */ static PyObject *get_parent(PyObject *globals, char *buf, int *p_buflen); static PyObject *load_next(PyObject *mod, PyObject *altmod, char **p_name, char *buf, int *p_buflen); static int mark_miss(char *name); static int ensure_fromlist(PyObject *mod, PyObject *fromlist, char *buf, int buflen, int recursive); static PyObject * import_submodule(PyObject *mod, char *name, char *fullname); static PyObject * import_module(char *name, PyObject *globals, PyObject *locals, PyObject *fromlist) { char buf[MAXPATHLEN+1]; int buflen = 0; PyObject *parent, *head, *next, *tail; parent = get_parent(globals, buf, &buflen); if (parent == NULL) return NULL; head = load_next(parent, Py_None, &name, buf, &buflen); if (head == NULL) return NULL; tail = head; Py_INCREF(tail); while (name) { next = load_next(tail, tail, &name, buf, &buflen); Py_DECREF(tail); if (next == NULL) { Py_DECREF(head); return NULL; } tail = next; } if (fromlist != NULL) { if (fromlist == Py_None || !PyObject_IsTrue(fromlist)) fromlist = NULL; } if (fromlist == NULL) { Py_DECREF(tail); return head; } Py_DECREF(head); if (!ensure_fromlist(tail, fromlist, buf, buflen, 0)) { Py_DECREF(tail); return NULL; } return tail; } static PyObject * get_parent(PyObject *globals, char *buf, int *p_buflen) { static PyObject *namestr = NULL; static PyObject *pathstr = NULL; PyObject *modname, *modpath, *modules, *parent; if (globals == NULL || !PyDict_Check(globals)) return Py_None; if (namestr == NULL) { namestr = PyString_InternFromString("__name__"); if (namestr == NULL) return NULL; } if (pathstr == NULL) { pathstr = PyString_InternFromString("__path__"); if (pathstr == NULL) return NULL; } *buf = '\0'; *p_buflen = 0; modname = PyDict_GetItem(globals, namestr); if (modname == NULL || !PyString_Check(modname)) return Py_None; modpath = PyDict_GetItem(globals, pathstr); if (modpath != NULL) { int len = PyString_GET_SIZE(modname); if (len > MAXPATHLEN) { PyErr_SetString(PyExc_ValueError, "Module name too long"); return NULL; } strcpy(buf, PyString_AS_STRING(modname)); *p_buflen = len; } else { char *start = PyString_AS_STRING(modname); char *lastdot = strrchr(start, '.'); size_t len; if (lastdot == NULL) return Py_None; len = lastdot - start; if (len >= MAXPATHLEN) { PyErr_SetString(PyExc_ValueError, "Module name too long"); return NULL; } strncpy(buf, start, len); buf[len] = '\0'; *p_buflen = len; } modules = PyImport_GetModuleDict(); parent = PyDict_GetItemString(modules, buf); if (parent == NULL) parent = Py_None; return parent; /* We expect, but can't guarantee, if parent != None, that: - parent.__name__ == buf - parent.__dict__ is globals If this is violated... Who cares? */ } /* altmod is either None or same as mod */ static PyObject * load_next(PyObject *mod, PyObject *altmod, char **p_name, char *buf, int *p_buflen) { char *name = *p_name; char *dot = strchr(name, '.'); size_t len; char *p; PyObject *result; if (dot == NULL) { *p_name = NULL; len = strlen(name); } else { *p_name = dot+1; len = dot-name; } if (len == 0) { PyErr_SetString(PyExc_ValueError, "Empty module name"); return NULL; } p = buf + *p_buflen; if (p != buf) *p++ = '.'; if (p+len-buf >= MAXPATHLEN) { PyErr_SetString(PyExc_ValueError, "Module name too long"); return NULL; } strncpy(p, name, len); p[len] = '\0'; *p_buflen = p+len-buf; result = import_submodule(mod, p, buf); if (result == Py_None && altmod != mod) { Py_DECREF(result); /* Here, altmod must be None and mod must not be None */ result = import_submodule(altmod, p, p); if (result != NULL && result != Py_None) { if (mark_miss(buf) != 0) { Py_DECREF(result); return NULL; } strncpy(buf, name, len); buf[len] = '\0'; *p_buflen = len; } } if (result == NULL) return NULL; if (result == Py_None) { Py_DECREF(result); PyErr_Format(PyExc_ImportError, "No module named %.200s", name); return NULL; } return result; } static int mark_miss(char *name) { PyObject *modules = PyImport_GetModuleDict(); return PyDict_SetItemString(modules, name, Py_None); } static int ensure_fromlist(PyObject *mod, PyObject *fromlist, char *buf, int buflen, int recursive) { Py_ssize_t i; if (!PyObject_HasAttrString(mod, "__path__")) return 1; for (i = 0; ; i++) { PyObject *item = PySequence_GetItem(fromlist, i); int hasit; if (item == NULL) { if (PyErr_ExceptionMatches(PyExc_IndexError)) { PyErr_Clear(); return 1; } return 0; } if (!PyString_Check(item)) { PyErr_SetString(PyExc_TypeError, "Item in ``from list'' not a string"); Py_DECREF(item); return 0; } if (PyString_AS_STRING(item)[0] == '*') { PyObject *all; Py_DECREF(item); /* See if the package defines __all__ */ if (recursive) continue; /* Avoid endless recursion */ all = PyObject_GetAttrString(mod, "__all__"); if (all == NULL) PyErr_Clear(); else { if (!ensure_fromlist(mod, all, buf, buflen, 1)) return 0; Py_DECREF(all); } continue; } hasit = PyObject_HasAttr(mod, item); if (!hasit) { char *subname = PyString_AS_STRING(item); PyObject *submod; char *p; if (buflen + strlen(subname) >= MAXPATHLEN) { PyErr_SetString(PyExc_ValueError, "Module name too long"); Py_DECREF(item); return 0; } p = buf + buflen; *p++ = '.'; strcpy(p, subname); submod = import_submodule(mod, subname, buf); Py_XDECREF(submod); if (submod == NULL) { Py_DECREF(item); return 0; } } Py_DECREF(item); } /* NOTREACHED */ } static PyObject * import_submodule(PyObject *mod, char *subname, char *fullname) { PyObject *modules = PyImport_GetModuleDict(); PyObject *m; /* Require: if mod == None: subname == fullname else: mod.__name__ + "." + subname == fullname */ if ((m = PyDict_GetItemString(modules, fullname)) != NULL) { Py_INCREF(m); } else { PyObject *path; if (mod == Py_None) path = NULL; else { path = PyObject_GetAttrString(mod, "__path__"); if (path == NULL) { PyErr_Clear(); Py_INCREF(Py_None); return Py_None; } } m = call_find_load(fullname, subname, path); if (m != NULL && m != Py_None && mod != Py_None) { if (PyObject_SetAttrString(mod, subname, m) < 0) { Py_DECREF(m); m = NULL; } } } return m; } PyObject * reload_module(PyObject *m) { PyObject *modules = PyImport_GetModuleDict(); PyObject *path = NULL; char *name, *subname; if (m == NULL || !PyModule_Check(m)) { PyErr_SetString(PyExc_TypeError, "reload_module() argument must be module"); return NULL; } name = PyModule_GetName(m); if (name == NULL) return NULL; if (m != PyDict_GetItemString(modules, name)) { PyErr_Format(PyExc_ImportError, "reload(): module %.200s not in sys.modules", name); return NULL; } subname = strrchr(name, '.'); if (subname == NULL) subname = name; else { PyObject *parentname, *parent; parentname = PyString_FromStringAndSize(name, (subname-name)); if (parentname == NULL) return NULL; parent = PyDict_GetItem(modules, parentname); Py_DECREF(parentname); if (parent == NULL) { PyErr_Format(PyExc_ImportError, "reload(): parent %.200s not in sys.modules", name); return NULL; } subname++; path = PyObject_GetAttrString(parent, "__path__"); if (path == NULL) PyErr_Clear(); } m = call_find_load(name, subname, path); Py_XDECREF(path); return m; } static PyObject * cimport_import_module(PyObject *self, PyObject *args) { char *name; PyObject *globals = NULL; PyObject *locals = NULL; PyObject *fromlist = NULL; if (!PyArg_ParseTuple(args, "s|OOO:import_module", &name, &globals, &locals, &fromlist)) return NULL; return import_module(name, globals, locals, fromlist); } static PyObject * cimport_reload_module(PyObject *self, PyObject *args) { PyObject *m; if (!PyArg_ParseTuple(args, "O:reload_module", &m)) return NULL; return reload_module(m); } static char doc_reload_module[] = "reload(module) -> module\n\ \n\ Reload the module. The module must have been successfully imported before."; static PyObject * cimport_set_loader(PyObject *self, PyObject *args) { PyObject *l = NULL; if (!PyArg_ParseTuple(args, "O:set_loader", &l)) return NULL; if (!PyCallable_Check(l)) { PyErr_SetString(PyExc_TypeError, "callable object needed"); return NULL; } Py_XDECREF(loader_hook); loader_hook = l; Py_INCREF(loader_hook); Py_INCREF(Py_None); return Py_None; } static char doc_set_loader[] = "\ Set the function that will be used to import modules.\n\ \n\ The function should should have the signature:\n\ \n\ loader(fullname : str, subname : str, path : [str] | None) -> module | None\n\ \n\ It should return the initialized module or None if it is not found.\n\ "; static PyObject * cimport_get_loader(PyObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, ":get_loader")) return NULL; Py_INCREF(loader_hook); return loader_hook; } static char doc_get_loader[] = "\ Get the function that will be used to import modules.\n\ "; static char doc_import_module[] = "\ import_module(name, globals, locals, fromlist) -> module\n\ \n\ Import a module. The globals are only used to determine the context;\n\ they are not modified. The locals are currently unused. The fromlist\n\ should be a list of names to emulate ``from name import ...'', or an\n\ empty list to emulate ``import name''.\n\ \n\ When importing a module from a package, note that import_module('A.B', ...)\n\ returns package A when fromlist is empty, but its submodule B when\n\ fromlist is not empty.\n\ "; static PyMethodDef cimport_methods[] = { {"import_module", cimport_import_module, 1, doc_import_module}, {"reload_module", cimport_reload_module, 1, doc_reload_module}, {"get_loader", cimport_get_loader, 1, doc_get_loader}, {"set_loader", cimport_set_loader, 1, doc_set_loader}, {NULL, NULL} /* sentinel */ }; void initcimport(void) { PyObject *m, *d; m = Py_InitModule4("cimport", cimport_methods, "", NULL, PYTHON_API_VERSION); d = PyModule_GetDict(m); } Quixote-2.7b2/quixote/ptl/ihooks_local.py0000664000175000017500000004524011251140226016706 0ustar nasnas# Based on ihooks.py from the Python 2.6 distribution. Fixes for relative # imports applied (see Python issue #6855) """Import hook support. Consistent use of this module will make it possible to change the different mechanisms involved in loading modules independently. While the built-in module imp exports interfaces to the built-in module searching and loading algorithm, and it is possible to replace the built-in function __import__ in order to change the semantics of the import statement, until now it has been difficult to combine the effect of different __import__ hacks, like loading modules from URLs by rimport.py, or restricted execution by rexec.py. This module defines three new concepts: 1) A "file system hooks" class provides an interface to a filesystem. One hooks class is defined (Hooks), which uses the interface provided by standard modules os and os.path. It should be used as the base class for other hooks classes. 2) A "module loader" class provides an interface to search for a module in a search path and to load it. It defines a method which searches for a module in a single directory; by overriding this method one can redefine the details of the search. If the directory is None, built-in and frozen modules are searched instead. Two module loader class are defined, both implementing the search strategy used by the built-in __import__ function: ModuleLoader uses the imp module's find_module interface, while HookableModuleLoader uses a file system hooks class to interact with the file system. Both use the imp module's load_* interfaces to actually load the module. 3) A "module importer" class provides an interface to import a module, as well as interfaces to reload and unload a module. It also provides interfaces to install and uninstall itself instead of the default __import__ and reload (and unload) functions. One module importer class is defined (ModuleImporter), which uses a module loader instance passed in (by default HookableModuleLoader is instantiated). The classes defined here should be used as base classes for extended functionality along those lines. If a module importer class supports dotted names, its import_module() must return a different value depending on whether it is called on behalf of a "from ... import ..." statement or not. (This is caused by the way the __import__ hook is used by the Python interpreter.) It would also do wise to install a different version of reload(). """ from warnings import warnpy3k, warn warnpy3k("the ihooks module has been removed in Python 3.0", stacklevel=2) del warnpy3k import __builtin__ import imp import os import sys __all__ = ["BasicModuleLoader","Hooks","ModuleLoader","FancyModuleLoader", "BasicModuleImporter","ModuleImporter","install","uninstall"] VERBOSE = 0 from imp import C_EXTENSION, PY_SOURCE, PY_COMPILED from imp import C_BUILTIN, PY_FROZEN, PKG_DIRECTORY BUILTIN_MODULE = C_BUILTIN FROZEN_MODULE = PY_FROZEN class _Verbose: def __init__(self, verbose = VERBOSE): self.verbose = verbose def get_verbose(self): return self.verbose def set_verbose(self, verbose): self.verbose = verbose # XXX The following is an experimental interface def note(self, *args): if self.verbose: self.message(*args) def message(self, format, *args): if args: print format%args else: print format class BasicModuleLoader(_Verbose): """Basic module loader. This provides the same functionality as built-in import. It doesn't deal with checking sys.modules -- all it provides is find_module() and a load_module(), as well as find_module_in_dir() which searches just one directory, and can be overridden by a derived class to change the module search algorithm when the basic dependency on sys.path is unchanged. The interface is a little more convenient than imp's: find_module(name, [path]) returns None or 'stuff', and load_module(name, stuff) loads the module. """ def find_module(self, name, path = None): if path is None: path = [None] + self.default_path() for dir in path: stuff = self.find_module_in_dir(name, dir) if stuff: return stuff return None def default_path(self): return sys.path def find_module_in_dir(self, name, dir): if dir is None: return self.find_builtin_module(name) else: try: return imp.find_module(name, [dir]) except ImportError: return None def find_builtin_module(self, name): # XXX frozen packages? if imp.is_builtin(name): return None, '', ('', '', BUILTIN_MODULE) if imp.is_frozen(name): return None, '', ('', '', FROZEN_MODULE) return None def load_module(self, name, stuff): file, filename, info = stuff try: return imp.load_module(name, file, filename, info) finally: if file: file.close() class Hooks(_Verbose): """Hooks into the filesystem and interpreter. By deriving a subclass you can redefine your filesystem interface, e.g. to merge it with the URL space. This base class behaves just like the native filesystem. """ # imp interface def get_suffixes(self): return imp.get_suffixes() def new_module(self, name): return imp.new_module(name) def is_builtin(self, name): return imp.is_builtin(name) def init_builtin(self, name): return imp.init_builtin(name) def is_frozen(self, name): return imp.is_frozen(name) def init_frozen(self, name): return imp.init_frozen(name) def get_frozen_object(self, name): return imp.get_frozen_object(name) def load_source(self, name, filename, file=None): return imp.load_source(name, filename, file) def load_compiled(self, name, filename, file=None): return imp.load_compiled(name, filename, file) def load_dynamic(self, name, filename, file=None): return imp.load_dynamic(name, filename, file) def load_package(self, name, filename, file=None): return imp.load_module(name, file, filename, ("", "", PKG_DIRECTORY)) def add_module(self, name): d = self.modules_dict() if name in d: return d[name] d[name] = m = self.new_module(name) return m # sys interface def modules_dict(self): return sys.modules def default_path(self): return sys.path def path_split(self, x): return os.path.split(x) def path_join(self, x, y): return os.path.join(x, y) def path_isabs(self, x): return os.path.isabs(x) # etc. def path_exists(self, x): return os.path.exists(x) def path_isdir(self, x): return os.path.isdir(x) def path_isfile(self, x): return os.path.isfile(x) def path_islink(self, x): return os.path.islink(x) # etc. def openfile(self, *x): return open(*x) openfile_error = IOError def listdir(self, x): return os.listdir(x) listdir_error = os.error # etc. class ModuleLoader(BasicModuleLoader): """Default module loader; uses file system hooks. By defining suitable hooks, you might be able to load modules from other sources than the file system, e.g. from compressed or encrypted files, tar files or (if you're brave!) URLs. """ def __init__(self, hooks = None, verbose = VERBOSE): BasicModuleLoader.__init__(self, verbose) self.hooks = hooks or Hooks(verbose) def default_path(self): return self.hooks.default_path() def modules_dict(self): return self.hooks.modules_dict() def get_hooks(self): return self.hooks def set_hooks(self, hooks): self.hooks = hooks def find_builtin_module(self, name): # XXX frozen packages? if self.hooks.is_builtin(name): return None, '', ('', '', BUILTIN_MODULE) if self.hooks.is_frozen(name): return None, '', ('', '', FROZEN_MODULE) return None def find_module_in_dir(self, name, dir, allow_packages=1): if dir is None: return self.find_builtin_module(name) if allow_packages: fullname = self.hooks.path_join(dir, name) if self.hooks.path_isdir(fullname): stuff = self.find_module_in_dir("__init__", fullname, 0) if stuff: file = stuff[0] if file: file.close() return None, fullname, ('', '', PKG_DIRECTORY) for info in self.hooks.get_suffixes(): suff, mode, type = info fullname = self.hooks.path_join(dir, name+suff) try: fp = self.hooks.openfile(fullname, mode) return fp, fullname, info except self.hooks.openfile_error: pass return None def load_module(self, name, stuff): file, filename, info = stuff (suff, mode, type) = info try: if type == BUILTIN_MODULE: return self.hooks.init_builtin(name) if type == FROZEN_MODULE: return self.hooks.init_frozen(name) if type == C_EXTENSION: m = self.hooks.load_dynamic(name, filename, file) elif type == PY_SOURCE: m = self.hooks.load_source(name, filename, file) elif type == PY_COMPILED: m = self.hooks.load_compiled(name, filename, file) elif type == PKG_DIRECTORY: m = self.hooks.load_package(name, filename, file) else: raise ImportError, "Unrecognized module type (%r) for %s" % \ (type, name) finally: if file: file.close() m.__file__ = filename return m class FancyModuleLoader(ModuleLoader): """Fancy module loader -- parses and execs the code itself.""" def load_module(self, name, stuff): file, filename, (suff, mode, type) = stuff realfilename = filename path = None if type == PKG_DIRECTORY: initstuff = self.find_module_in_dir("__init__", filename, 0) if not initstuff: raise ImportError, "No __init__ module in package %s" % name initfile, initfilename, initinfo = initstuff initsuff, initmode, inittype = initinfo if inittype not in (PY_COMPILED, PY_SOURCE): if initfile: initfile.close() raise ImportError, \ "Bad type (%r) for __init__ module in package %s" % ( inittype, name) path = [filename] file = initfile realfilename = initfilename type = inittype if type == FROZEN_MODULE: code = self.hooks.get_frozen_object(name) elif type == PY_COMPILED: import marshal file.seek(8) code = marshal.load(file) elif type == PY_SOURCE: data = file.read() code = compile(data, realfilename, 'exec') else: return ModuleLoader.load_module(self, name, stuff) m = self.hooks.add_module(name) if path: m.__path__ = path m.__file__ = filename try: exec code in m.__dict__ except: d = self.hooks.modules_dict() if name in d: del d[name] raise return m class BasicModuleImporter(_Verbose): """Basic module importer; uses module loader. This provides basic import facilities but no package imports. """ def __init__(self, loader = None, verbose = VERBOSE): _Verbose.__init__(self, verbose) self.loader = loader or ModuleLoader(None, verbose) self.modules = self.loader.modules_dict() def get_loader(self): return self.loader def set_loader(self, loader): self.loader = loader def get_hooks(self): return self.loader.get_hooks() def set_hooks(self, hooks): return self.loader.set_hooks(hooks) def import_module(self, name, globals={}, locals={}, fromlist=[]): name = str(name) if name in self.modules: return self.modules[name] # Fast path stuff = self.loader.find_module(name) if not stuff: raise ImportError, "No module named %s" % name return self.loader.load_module(name, stuff) def reload(self, module, path = None): name = str(module.__name__) stuff = self.loader.find_module(name, path) if not stuff: raise ImportError, "Module %s not found for reload" % name return self.loader.load_module(name, stuff) def unload(self, module): del self.modules[str(module.__name__)] # XXX Should this try to clear the module's namespace? def install(self): self.save_import_module = __builtin__.__import__ self.save_reload = __builtin__.reload if not hasattr(__builtin__, 'unload'): __builtin__.unload = None self.save_unload = __builtin__.unload __builtin__.__import__ = self.import_module __builtin__.reload = self.reload __builtin__.unload = self.unload def uninstall(self): __builtin__.__import__ = self.save_import_module __builtin__.reload = self.save_reload __builtin__.unload = self.save_unload if not __builtin__.unload: del __builtin__.unload class ModuleImporter(BasicModuleImporter): """A module importer that supports packages.""" def import_module(self, name, globals=None, locals=None, fromlist=None, level=-1): parent = self.determine_parent(globals, level) q, tail = self.find_head_package(parent, str(name)) m = self.load_tail(q, tail) if not fromlist: return q if hasattr(m, "__path__"): self.ensure_fromlist(m, fromlist) return m def determine_parent(self, globals, level=-1): if not globals or not level: return None pkgname = globals.get('__package__') if pkgname is not None: if not pkgname and level > 0: raise ValueError, 'Attempted relative import in non-package' else: # __package__ not set, figure it out and set it modname = globals.get('__name__') if modname is None: return None if "__path__" in globals: # __path__ is set so modname is already the package name pkgname = modname else: # normal module, work out package name if any if '.' not in modname: if level > 0: raise ValueError, ('Attempted relative import in ' 'non-package') globals['__package__'] = None return None pkgname = modname.rpartition('.')[0] globals['__package__'] = pkgname if level > 0: dot = len(pkgname) for x in range(level, 1, -1): try: dot = pkgname.rindex('.', 0, dot) except ValueError: raise ValueError('attempted relative import beyond ' 'top-level package') pkgname = pkgname[:dot] try: return sys.modules[pkgname] except KeyError: if level < 1: warn("Parent module '%s' not found while handling " "absolute import" % pkgname, RuntimeWarning, 1) return None else: raise SystemError, ("Parent module '%s' not loaded, cannot " "perform relative import" % pkgname) def find_head_package(self, parent, name): if '.' in name: i = name.find('.') head = name[:i] tail = name[i+1:] else: head = name tail = "" if parent: qname = "%s.%s" % (parent.__name__, head) else: qname = head q = self.import_it(head, qname, parent) if q: return q, tail if parent: qname = head parent = None q = self.import_it(head, qname, parent) if q: return q, tail raise ImportError, "No module named '%s'" % qname def load_tail(self, q, tail): m = q while tail: i = tail.find('.') if i < 0: i = len(tail) head, tail = tail[:i], tail[i+1:] mname = "%s.%s" % (m.__name__, head) m = self.import_it(head, mname, m) if not m: raise ImportError, "No module named '%s'" % mname return m def ensure_fromlist(self, m, fromlist, recursive=0): for sub in fromlist: if sub == "*": if not recursive: try: all = m.__all__ except AttributeError: pass else: self.ensure_fromlist(m, all, 1) continue if sub != "*" and not hasattr(m, sub): subname = "%s.%s" % (m.__name__, sub) submod = self.import_it(sub, subname, m) if not submod: raise ImportError, "No module named '%s'" % subname def import_it(self, partname, fqname, parent, force_load=0): if not partname: # completely empty module name should only happen in # 'from . import' or __import__("") return parent if not force_load: try: return self.modules[fqname] except KeyError: pass try: path = parent and parent.__path__ except AttributeError: return None partname = str(partname) stuff = self.loader.find_module(partname, path) if not stuff: return None fqname = str(fqname) m = self.loader.load_module(fqname, stuff) if parent: setattr(parent, partname, m) return m def reload(self, module): name = str(module.__name__) if '.' not in name: return self.import_it(name, name, None, force_load=1) i = name.rfind('.') pname = name[:i] parent = self.modules[pname] return self.import_it(name[i+1:], name, parent, force_load=1) default_importer = None current_importer = None def install(importer = None): global current_importer current_importer = importer or default_importer or ModuleImporter() current_importer.install() def uninstall(): global current_importer current_importer.uninstall() Quixote-2.7b2/quixote/ptl/infinite_reload.py0000664000175000017500000000046511210600334017367 0ustar nasnas# For testing http://python.org/sf/742342, which reports that Python # segfaults (infinite recursion in C) in the presence of infinite # reload()ing. This module is imported by test_import.py:test_infinite_reload # to make sure this doesn't happen any more. import infinite_reload reload(infinite_reload) Quixote-2.7b2/quixote/ptl/install.py0000664000175000017500000000007711114154100015677 0ustar nasnasimport quixote.ptl.ptl_import quixote.ptl.ptl_import.install() Quixote-2.7b2/quixote/ptl/ptl_compile.py0000664000175000017500000002616211311246001016544 0ustar nasnas#!/www/python/bin/python """Compile a PTL template. First template function names are mangled, noting the template type. Next, the file is parsed into a parse tree. This tree is converted into a modified AST. It is during this state that the semantics are modified by adding extra nodes to the tree. Finally bytecode is generated using the compiler package. """ import sys import os import stat import symbol import token import re import imp import stat import marshal import struct assert sys.hexversion >= 0x20300b1, 'PTL requires Python 2.3 or newer' from compiler import pycodegen, transformer from compiler import ast from compiler.consts import OP_ASSIGN from compiler import misc, syntax HTML_TEMPLATE_PREFIX = "_q_html_template_" PLAIN_TEMPLATE_PREFIX = "_q_plain_template_" class TemplateTransformer(transformer.Transformer): def __init__(self, *args, **kwargs): transformer.Transformer.__init__(self, *args, **kwargs) # __template_type is a stack whose values are # "html", "plain", or None self.__template_type = [] def _get_template_type(self): """Return the type of the function being compiled ( "html", "plain", or None) """ if self.__template_type: return self.__template_type[-1] else: return None def file_input(self, nodelist): doc = None # self.get_docstring(nodelist, symbol.file_input) if sys.hexversion >= 0x02050000: html_imp = ast.From( 'quixote.html', [('TemplateIO', '_q_TemplateIO'), ('htmltext', '_q_htmltext')], 0) vars_imp = ast.From("__builtin__", [("vars", "_q_vars")], 0) else: html_imp = ast.From( 'quixote.html', [('TemplateIO', '_q_TemplateIO'), ('htmltext', '_q_htmltext')]) vars_imp = ast.From("__builtin__", [("vars", "_q_vars")]) ptl_imports = [ vars_imp, html_imp ] stmts = [] for node in nodelist: if node[0] != token.ENDMARKER and node[0] != token.NEWLINE: self.com_append_stmt(stmts, node) # count __future__ statements i = 0 for stmt in stmts: if isinstance(stmt, ast.From) and stmt.modname == '__future__': i += 1 else: break stmts[i:i] = ptl_imports return ast.Module(doc, ast.Stmt(stmts)) def funcdef(self, nodelist): if len(nodelist) == 6: assert nodelist[0][0] == symbol.decorators decorators = self.decorators(nodelist[0][1:]) else: assert len(nodelist) == 5 decorators = None lineno = nodelist[-4][2] name = nodelist[-4][1] args = nodelist[-3][2] if not re.match('_q_(html|plain)_(dollar_)?template_', name): # just a normal function, let base class handle it self.__template_type.append(None) n = transformer.Transformer.funcdef(self, nodelist) else: if name.startswith(PLAIN_TEMPLATE_PREFIX): name = name[len(PLAIN_TEMPLATE_PREFIX):] template_type = "plain" elif name.startswith(HTML_TEMPLATE_PREFIX): name = name[len(HTML_TEMPLATE_PREFIX):] template_type = "html" else: raise RuntimeError, 'unknown prefix on %s' % name self.__template_type.append(template_type) if args[0] == symbol.varargslist: names, defaults, flags = self.com_arglist(args[1:]) else: names = defaults = () flags = 0 doc = None # self.get_docstring(nodelist[-1]) # code for function code = self.com_node(nodelist[-1]) # _q_output = _q_TemplateIO() klass = ast.Name('_q_TemplateIO') args = [ast.Const(template_type == "html")] instance = ast.CallFunc(klass, args) assign_name = ast.AssName('_q_output', OP_ASSIGN) assign = ast.Assign([assign_name], instance) # return _q_output.getvalue() func = ast.Getattr(ast.Name('_q_output'), "getvalue") ret = ast.Return(ast.CallFunc(func, [])) # wrap original function code code = ast.Stmt([assign, code, ret]) if sys.hexversion >= 0x20400a2: n = ast.Function(decorators, name, names, defaults, flags, doc, code) else: n = ast.Function(name, names, defaults, flags, doc, code) n.lineno = lineno self.__template_type.pop() return n def expr_stmt(self, nodelist): if self._get_template_type() is None: return transformer.Transformer.expr_stmt(self, nodelist) # Instead of discarding objects on the stack, call # "_q_output += obj". exprNode = self.com_node(nodelist[-1]) if len(nodelist) == 1: lval = ast.Name('_q_output') n = ast.AugAssign(lval, '+=', exprNode) if hasattr(exprNode, 'lineno'): n.lineno = exprNode.lineno elif nodelist[1][0] == token.EQUAL: nodes = [ ] for i in range(0, len(nodelist) - 2, 2): nodes.append(self.com_assign(nodelist[i], OP_ASSIGN)) n = ast.Assign(nodes, exprNode) n.lineno = nodelist[1][2] else: lval = self.com_augassign(nodelist[0]) op = self.com_augassign_op(nodelist[1]) n = ast.AugAssign(lval, op[1], exprNode) n.lineno = op[2] return n def atom_string(self, nodelist): const_node = transformer.Transformer.atom_string(self, nodelist) if "html" == self._get_template_type(): return ast.CallFunc(ast.Name('_q_htmltext'), [const_node]) else: return const_node _template_re = re.compile( r"^(?P[ \t]*) def (?:[ \t]+)" r" (?P[a-zA-Z_][a-zA-Z_0-9]*)" r" (?:[ \t]*) \[(?Pplain|html)\] (?:[ \t]*)" r" (?:[ \t]*[\(\\])", re.MULTILINE|re.VERBOSE) def translate_tokens(buf): """ Since we can't modify the parser in the builtin parser module we must do token translation here. Luckily it does not affect line numbers. def foo [plain] (...): -> def _q_plain_template__foo(...): def foo [html] (...): -> def _q_html_template__foo(...): XXX This parser is too stupid. For example, it doesn't understand triple quoted strings. """ def replacement(match): template_type = match.group('type') return '%sdef _q_%s_template_%s(' % (match.group('indent'), template_type, match.group('name')) return _template_re.sub(replacement, buf) def parse(buf, filename=''): buf = translate_tokens(buf) try: return TemplateTransformer().parsesuite(buf) except SyntaxError, e: # set the filename attribute raise SyntaxError(str(e), (filename, e.lineno, e.offset, e.text)) PTL_EXT = ".ptl" class Template(pycodegen.Module): def _get_tree(self): tree = parse(self.source, self.filename) misc.set_filename(self.filename, tree) syntax.check(tree) return tree def dump(self, fp): mtime = os.stat(self.filename)[stat.ST_MTIME] fp.write('\0\0\0\0') fp.write(struct.pack(' code Compile an open file. If output is not None then the code is written to output. The code object is returned. """ buf = input.read() template = Template(buf, filename) template.compile() if output is not None: template.dump(output) return template.code def compile(inputname, outputname): """(inputname, outputname) Compile a template file. The new template is written to outputname. """ input = open(inputname) output = open(outputname, "wb") try: compile_template(input, inputname, output) except: # don't leave a corrupt .pyc file around output.close() os.unlink(outputname) raise def compile_file(filename, force=0, verbose=0): if filename.endswith(PTL_EXT): cfile = filename[:-4] + '.pyc' ftime = os.stat(filename)[stat.ST_MTIME] try: ctime = os.stat(cfile)[stat.ST_MTIME] except os.error: ctime = 0 if (ctime > ftime) and not force: return if verbose: print 'Compiling', filename, '...' ok = compile(filename, cfile) def compile_dir(dir, maxlevels=10, force=0): """Byte-compile all PTL modules in the given directory tree. (Adapted from compile_dir in Python module: compileall.py) Arguments (only dir is required): dir: the directory to byte-compile maxlevels: maximum recursion level (default 10) force: if true, force compilation, even if timestamps are up-to-date """ print 'Listing', dir, '...' try: names = os.listdir(dir) except os.error: print "Can't list", dir names = [] names.sort() success = 1 for name in names: fullname = os.path.join(dir, name) if os.path.isfile(fullname): try: ok = compile_file(fullname, force=force, verbose=1) except KeyboardInterrupt: raise KeyboardInterrupt except: # XXX compile catches SyntaxErrors if type(sys.exc_type) == type(''): exc_type_name = sys.exc_type else: exc_type_name = sys.exc_type.__name__ print 'Sorry:', exc_type_name + ':', print sys.exc_value success = 0 else: if ok == 0: success = 0 elif (maxlevels > 0 and name != os.curdir and name != os.pardir and os.path.isdir(fullname) and not os.path.islink(fullname)): if not compile_dir(fullname, maxlevels - 1, force): success = 0 return success def compile_package(path, force=0, verbose=0): """Compile all PTL files in a package. 'path' should be a list of directory names containing the files of the package (i.e. __path__). """ for package_dir in path: for dirpath, dirnames, filenames in os.walk(package_dir): for dirname in dirnames: compile_file(os.path.join(dirpath, dirname), force=force, verbose=verbose) for filename in filenames: compile_file(os.path.join(dirpath, filename), force=force, verbose=verbose) def main(): args = sys.argv[1:] if not args: print "no files to compile" else: for filename in args: path, ext = os.path.splitext(filename) compile(filename, path + ".pyc") if __name__ == "__main__": main() Quixote-2.7b2/quixote/ptl/ptl_import.py0000664000175000017500000001074511326376155016452 0ustar nasnas"""Import hooks; when installed, these hooks allow importing .ptl files as if they were Python modules. Note: there's some unpleasant incompatibility between ZODB's import trickery and the import hooks here. Bottom line: if you're using ZODB, import it *before* installing the PTL import hooks. """ import sys import os.path import imp, ihooks, new import struct import marshal import __builtin__ # Check for a deficient ihooks module. Python 2.6 was released without # ihooks.py being updated to support relative imports. Any library that uses # relative imports will cause the import hook to fail. Use our local copy of # ihooks module which does have support for relative imports. if sys.hexversion >= 0x20600b0: _m = ihooks.ModuleImporter.import_module if _m.im_func.func_code.co_argcount == 5: import ihooks_local as ihooks from quixote.ptl.ptl_compile import compile_template, PTL_EXT assert sys.hexversion >= 0x20000b1, "need Python 2.0b1 or later" def _exec_module_code(code, name, filename): if name in sys.modules: mod = sys.modules[name] # necessary for reload() else: mod = new.module(name) sys.modules[name] = mod mod.__name__ = name mod.__file__ = filename exec code in mod.__dict__ return mod def _timestamp(filename): try: s = os.stat(filename) except OSError: return None return s.st_mtime def _load_pyc(name, filename, pyc_filename): try: fp = open(pyc_filename, "rb") except IOError: return None if fp.read(4) == imp.get_magic(): mtime = struct.unpack('= ptl_mtime: code = marshal.load(fp) return _exec_module_code(code, name, filename) return None def _load_ptl(name, filename, file=None): if not file: try: file = open(filename, "rb") except IOError: return None path, ext = os.path.splitext(filename) pyc_filename = path + ".pyc" module = _load_pyc(name, filename, pyc_filename) if module is not None: return module try: output = open(pyc_filename, "wb") except IOError: output = None try: code = compile_template(file, filename, output) except: if output: output.close() os.unlink(pyc_filename) raise else: if output: output.close() return _exec_module_code(code, name, filename) # Constant used to signal a PTL files PTL_FILE = object() class PTLHooks(ihooks.Hooks): def get_suffixes(self): # add our suffixes return [(PTL_EXT, 'r', PTL_FILE)] + imp.get_suffixes() class PTLLoader(ihooks.ModuleLoader): def load_module(self, name, stuff): file, filename, info = stuff (suff, mode, type) = info # If it's a PTL file, load it specially. if type is PTL_FILE: return _load_ptl(name, filename, file) else: # Otherwise, use the default handler for loading return ihooks.ModuleLoader.load_module(self, name, stuff) if sys.hexversion <= 0x20600b0: try: import cimport except ImportError: cimport = None else: # cimport module doesn't handle relative imports cimport = None class cModuleImporter(ihooks.ModuleImporter): def __init__(self, loader=None): self.loader = loader or ihooks.ModuleLoader() cimport.set_loader(self.find_import_module) def find_import_module(self, fullname, subname, path): stuff = self.loader.find_module(subname, path) if not stuff: return None return self.loader.load_module(fullname, stuff) def install(self): self.save_import_module = __builtin__.__import__ self.save_reload = __builtin__.reload if not hasattr(__builtin__, 'unload'): __builtin__.unload = None self.save_unload = __builtin__.unload __builtin__.__import__ = cimport.import_module __builtin__.reload = cimport.reload_module __builtin__.unload = self.unload _installed = False def install(): global _installed if not _installed: hooks = PTLHooks() loader = PTLLoader(hooks) if cimport is not None: importer = cModuleImporter(loader) else: importer = ihooks.ModuleImporter(loader) ihooks.install(importer) _installed = True if __name__ == '__main__': install() Quixote-2.7b2/quixote/ptl/ptlrun.py0000775000175000017500000000025411114154100015555 0ustar nasnas#!/usr/bin/env python import sys from quixote.ptl.ptl_compile import compile_template if __name__ == '__main__': exec compile_template(open(sys.argv[1]), sys.argv[1]) Quixote-2.7b2/quixote/ptl/qx_distutils.py0000664000175000017500000000333411114154100016764 0ustar nasnas"""Provides a version of the Distutils "build_py" command that knows about PTL files. """ import os, string from glob import glob from types import StringType, ListType, TupleType from distutils.command.build_py import build_py class qx_build_py(build_py): def find_package_modules(self, package, package_dir): self.check_package(package, package_dir) module_files = (glob(os.path.join(package_dir, "*.py")) + glob(os.path.join(package_dir, "*.ptl"))) modules = [] setup_script = os.path.abspath(self.distribution.script_name) for f in module_files: abs_f = os.path.abspath(f) if abs_f != setup_script: module = os.path.splitext(os.path.basename(f))[0] modules.append((package, module, f)) else: self.debug_print("excluding %s" % setup_script) return modules def build_module(self, module, module_file, package): if type(package) is StringType: package = string.split(package, '.') elif type(package) not in (ListType, TupleType): raise TypeError, \ "'package' must be a string (dot-separated), list, or tuple" # Now put the module source file into the "build" area -- this is # easy, we just copy it somewhere under self.build_lib (the build # directory for Python source). outfile = self.get_module_outfile(self.build_lib, package, module) if module_file.endswith(".ptl"): # XXX hack for PTL outfile = outfile[0:outfile.rfind('.')] + ".ptl" dir = os.path.dirname(outfile) self.mkpath(dir) return self.copy_file(module_file, outfile, preserve_mode=0) Quixote-2.7b2/quixote/ptl/relimport.py0000664000175000017500000000003311210600255016242 0ustar nasnasfrom .test_import import * Quixote-2.7b2/quixote/ptl/test_import.py0000664000175000017500000003537111210612707016620 0ustar nasnasimport unittest import os import random import shutil import sys import py_compile import warnings import marshal from test.test_support import unlink, TESTFN, unload, run_unittest, check_warnings def remove_files(name): for f in (name + os.extsep + "py", name + os.extsep + "pyc", name + os.extsep + "pyo", name + os.extsep + "pyw", name + "$py.class"): if os.path.exists(f): os.remove(f) class ImportTest(unittest.TestCase): def testCaseSensitivity(self): # Brief digression to test that import is case-sensitive: if we got this # far, we know for sure that "random" exists. try: import RAnDoM except ImportError: pass else: self.fail("import of RAnDoM should have failed (case mismatch)") def testDoubleConst(self): # Another brief digression to test the accuracy of manifest float constants. from test import double_const # don't blink -- that *was* the test def testImport(self): def test_with_extension(ext): # ext normally ".py"; perhaps ".pyw" source = TESTFN + ext pyo = TESTFN + os.extsep + "pyo" if sys.platform.startswith('java'): pyc = TESTFN + "$py.class" else: pyc = TESTFN + os.extsep + "pyc" f = open(source, "w") print >> f, "# This tests Python's ability to import a", ext, "file." a = random.randrange(1000) b = random.randrange(1000) print >> f, "a =", a print >> f, "b =", b f.close() try: try: mod = __import__(TESTFN) except ImportError, err: self.fail("import from %s failed: %s" % (ext, err)) self.assertEquals(mod.a, a, "module loaded (%s) but contents invalid" % mod) self.assertEquals(mod.b, b, "module loaded (%s) but contents invalid" % mod) finally: os.unlink(source) try: try: reload(mod) except ImportError, err: self.fail("import from .pyc/.pyo failed: %s" % err) finally: try: os.unlink(pyc) except OSError: pass try: os.unlink(pyo) except OSError: pass del sys.modules[TESTFN] sys.path.insert(0, os.curdir) try: test_with_extension(os.extsep + "py") if sys.platform.startswith("win"): for ext in ".PY", ".Py", ".pY", ".pyw", ".PYW", ".pYw": test_with_extension(ext) finally: del sys.path[0] def testImpModule(self): # Verify that the imp module can correctly load and find .py files import imp x = imp.find_module("os") os = imp.load_module("os", *x) def test_module_with_large_stack(self, module='longlist'): # create module w/list of 65000 elements to test bug #561858 filename = module + os.extsep + 'py' # create a file with a list of 65000 elements f = open(filename, 'w+') f.write('d = [\n') for i in range(65000): f.write('"",\n') f.write(']') f.close() # compile & remove .py file, we only need .pyc (or .pyo) f = open(filename, 'r') py_compile.compile(filename) f.close() os.unlink(filename) # need to be able to load from current dir sys.path.append('') # this used to crash exec 'import ' + module # cleanup del sys.path[-1] for ext in 'pyc', 'pyo': fname = module + os.extsep + ext if os.path.exists(fname): os.unlink(fname) def test_failing_import_sticks(self): source = TESTFN + os.extsep + "py" f = open(source, "w") print >> f, "a = 1/0" f.close() # New in 2.4, we shouldn't be able to import that no matter how often # we try. sys.path.insert(0, os.curdir) try: for i in 1, 2, 3: try: mod = __import__(TESTFN) except ZeroDivisionError: if TESTFN in sys.modules: self.fail("damaged module in sys.modules on %i. try" % i) else: self.fail("was able to import a damaged module on %i. try" % i) finally: sys.path.pop(0) remove_files(TESTFN) def test_failing_reload(self): # A failing reload should leave the module object in sys.modules. source = TESTFN + os.extsep + "py" f = open(source, "w") print >> f, "a = 1" print >> f, "b = 2" f.close() sys.path.insert(0, os.curdir) try: mod = __import__(TESTFN) self.assert_(TESTFN in sys.modules, "expected module in sys.modules") self.assertEquals(mod.a, 1, "module has wrong attribute values") self.assertEquals(mod.b, 2, "module has wrong attribute values") # On WinXP, just replacing the .py file wasn't enough to # convince reload() to reparse it. Maybe the timestamp didn't # move enough. We force it to get reparsed by removing the # compiled file too. remove_files(TESTFN) # Now damage the module. f = open(source, "w") print >> f, "a = 10" print >> f, "b = 20//0" f.close() self.assertRaises(ZeroDivisionError, reload, mod) # But we still expect the module to be in sys.modules. mod = sys.modules.get(TESTFN) self.failIf(mod is None, "expected module to still be in sys.modules") # We should have replaced a w/ 10, but the old b value should # stick. self.assertEquals(mod.a, 10, "module has wrong attribute values") self.assertEquals(mod.b, 2, "module has wrong attribute values") finally: sys.path.pop(0) remove_files(TESTFN) if TESTFN in sys.modules: del sys.modules[TESTFN] def __test_infinite_reload(self): # Bug #742342 reports that Python segfaults (infinite recursion in C) # when faced with self-recursive reload()ing. sys.path.insert(0, os.path.dirname(__file__)) try: import infinite_reload finally: sys.path.pop(0) def test_import_name_binding(self): # import x.y.z binds x in the current namespace import test as x import test.test_support self.assert_(x is test, x.__name__) self.assert_(hasattr(test.test_support, "__file__")) # import x.y.z as w binds z as w import test.test_support as y self.assert_(y is test.test_support, y.__name__) def test_import_initless_directory_warning(self): with warnings.catch_warnings(): # Just a random non-package directory we always expect to be # somewhere in sys.path... warnings.simplefilter('error', ImportWarning) self.assertRaises(ImportWarning, __import__, "site-packages") def __test_importbyfilename(self): path = os.path.abspath(TESTFN) try: __import__(path) except ImportError, err: self.assertEqual("Import by filename is not supported.", err.args[0]) else: self.fail("import by path didn't raise an exception") class TestPycRewriting(unittest.TestCase): # Test that the `co_filename` attribute on code objects always points # to the right file, even when various things happen (e.g. both the .py # and the .pyc file are renamed). module_name = "unlikely_module_name" module_source = """ import sys code_filename = sys._getframe().f_code.co_filename module_filename = __file__ constant = 1 def func(): pass func_filename = func.func_code.co_filename """ dir_name = os.path.abspath(TESTFN) file_name = os.path.join(dir_name, module_name) + os.extsep + "py" compiled_name = file_name + ("c" if __debug__ else "o") def setUp(self): self.sys_path = sys.path[:] self.orig_module = sys.modules.pop(self.module_name, None) os.mkdir(self.dir_name) with open(self.file_name, "w") as f: f.write(self.module_source) sys.path.insert(0, self.dir_name) def tearDown(self): sys.path[:] = self.sys_path if self.orig_module is not None: sys.modules[self.module_name] = self.orig_module else: del sys.modules[self.module_name] for file_name in self.file_name, self.compiled_name: if os.path.exists(file_name): os.remove(file_name) if os.path.exists(self.dir_name): shutil.rmtree(self.dir_name) def import_module(self): ns = globals() __import__(self.module_name, ns, ns) return sys.modules[self.module_name] def test_basics(self): mod = self.import_module() self.assertEqual(mod.module_filename, self.file_name) self.assertEqual(mod.code_filename, self.file_name) self.assertEqual(mod.func_filename, self.file_name) del sys.modules[self.module_name] mod = self.import_module() self.assertEqual(mod.module_filename, self.compiled_name) self.assertEqual(mod.code_filename, self.file_name) self.assertEqual(mod.func_filename, self.file_name) def __test_incorrect_code_name(self): py_compile.compile(self.file_name, dfile="another_module.py") mod = self.import_module() self.assertEqual(mod.module_filename, self.compiled_name) self.assertEqual(mod.code_filename, self.file_name) self.assertEqual(mod.func_filename, self.file_name) def test_module_without_source(self): target = "another_module.py" py_compile.compile(self.file_name, dfile=target) os.remove(self.file_name) mod = self.import_module() self.assertEqual(mod.module_filename, self.compiled_name) self.assertEqual(mod.code_filename, target) self.assertEqual(mod.func_filename, target) def test_foreign_code(self): py_compile.compile(self.file_name) with open(self.compiled_name, "rb") as f: header = f.read(8) code = marshal.load(f) constants = list(code.co_consts) foreign_code = test_main.func_code pos = constants.index(1) constants[pos] = foreign_code code = type(code)(code.co_argcount, code.co_nlocals, code.co_stacksize, code.co_flags, code.co_code, tuple(constants), code.co_names, code.co_varnames, code.co_filename, code.co_name, code.co_firstlineno, code.co_lnotab, code.co_freevars, code.co_cellvars) with open(self.compiled_name, "wb") as f: f.write(header) marshal.dump(code, f) mod = self.import_module() self.assertEqual(mod.constant.co_filename, foreign_code.co_filename) class PathsTests(unittest.TestCase): path = TESTFN def setUp(self): os.mkdir(self.path) self.syspath = sys.path[:] def tearDown(self): shutil.rmtree(self.path) sys.path = self.syspath # http://bugs.python.org/issue1293 def test_trailing_slash(self): f = open(os.path.join(self.path, 'test_trailing_slash.py'), 'w') f.write("testdata = 'test_trailing_slash'") f.close() sys.path.append(self.path+'/') mod = __import__("test_trailing_slash") self.assertEqual(mod.testdata, 'test_trailing_slash') unload("test_trailing_slash") # http://bugs.python.org/issue3677 def _test_UNC_path(self): f = open(os.path.join(self.path, 'test_trailing_slash.py'), 'w') f.write("testdata = 'test_trailing_slash'") f.close() #create the UNC path, like \\myhost\c$\foo\bar path = os.path.abspath(self.path) import socket hn = socket.gethostname() drive = path[0] unc = "\\\\%s\\%s$"%(hn, drive) unc += path[2:] sys.path.append(path) mod = __import__("test_trailing_slash") self.assertEqual(mod.testdata, 'test_trailing_slash') unload("test_trailing_slash") if sys.platform == "win32": test_UNC_path = _test_UNC_path class RelativeImport(unittest.TestCase): def tearDown(self): try: del sys.modules["test.relimport"] except: pass def test_relimport_star(self): # This will import * from .test_import. from . import relimport self.assertTrue(hasattr(relimport, "RelativeImport")) def test_issue3221(self): def check_absolute(): exec "from os import path" in ns def check_relative(): exec "from . import relimport" in ns # Check both OK with __package__ and __name__ correct ns = dict(__package__='test', __name__='test.notarealmodule') check_absolute() check_relative() # Check both OK with only __name__ wrong ns = dict(__package__='test', __name__='notarealpkg.notarealmodule') check_absolute() check_relative() # Check relative fails with only __package__ wrong ns = dict(__package__='foo', __name__='test.notarealmodule') with check_warnings() as w: check_absolute() self.assert_('foo' in str(w.message)) self.assertEqual(w.category, RuntimeWarning) self.assertRaises(SystemError, check_relative) # Check relative fails with __package__ and __name__ wrong ns = dict(__package__='foo', __name__='notarealpkg.notarealmodule') with check_warnings() as w: check_absolute() self.assert_('foo' in str(w.message)) self.assertEqual(w.category, RuntimeWarning) self.assertRaises(SystemError, check_relative) # Check both fail with package set to a non-string ns = dict(__package__=object()) self.assertRaises(ValueError, check_absolute) self.assertRaises(ValueError, check_relative) def test_main(verbose=None): import ihooks ihooks.install(ihooks.ModuleImporter()) run_unittest(ImportTest, TestPycRewriting, PathsTests, RelativeImport) if __name__ == '__main__': # test needs to be a package, so we can do relative import #from test.test_import import test_main from quixote.ptl.test_import import test_main test_main() Quixote-2.7b2/quixote/server/0000775000175000017500000000000011326377244014407 5ustar nasnasQuixote-2.7b2/quixote/server/__init__.py0000664000175000017500000000006311114154100016472 0ustar nasnas"""This package is for Quixote to server glue. """ Quixote-2.7b2/quixote/server/_fcgi.py0000664000175000017500000003623211326376515016036 0ustar nasnas# Derived from Robin Dunn's FastCGI module, # available at http://alldunn.com/python/#fcgi. #------------------------------------------------------------------------ # Copyright (c) 1998 by Total Control Software # All Rights Reserved #------------------------------------------------------------------------ # # Module Name: fcgi.py # # Description: Handles communication with the FastCGI module of the # web server without using the FastCGI developers kit, but # will also work in a non-FastCGI environment, (straight CGI.) # This module was originally fetched from someplace on the # Net (I don't remember where and I can't find it now...) and # has been significantly modified to fix several bugs, be more # readable, more robust at handling large CGI data and return # document sizes, and also to fit the model that we had previously # used for FastCGI. # # WARNING: If you don't know what you are doing, don't tinker with this # module! # # Creation Date: 1/30/98 2:59:04PM # # License: This is free software. You may use this software for any # purpose including modification/redistribution, so long as # this header remains intact and that you do not claim any # rights of ownership or authorship of this software. This # software has been tested, but no warranty is expressed or # implied. # #------------------------------------------------------------------------ import os, sys, string, socket, errno, struct from StringIO import StringIO import cgi #--------------------------------------------------------------------------- # Set various FastCGI constants # Maximum number of requests that can be handled FCGI_MAX_REQS=1 FCGI_MAX_CONNS = 1 # Supported version of the FastCGI protocol FCGI_VERSION_1 = 1 # Boolean: can this application multiplex connections? FCGI_MPXS_CONNS=0 # Record types FCGI_BEGIN_REQUEST = 1 ; FCGI_ABORT_REQUEST = 2 ; FCGI_END_REQUEST = 3 FCGI_PARAMS = 4 ; FCGI_STDIN = 5 ; FCGI_STDOUT = 6 FCGI_STDERR = 7 ; FCGI_DATA = 8 ; FCGI_GET_VALUES = 9 FCGI_GET_VALUES_RESULT = 10 FCGI_UNKNOWN_TYPE = 11 FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE # Types of management records ManagementTypes = [FCGI_GET_VALUES] FCGI_NULL_REQUEST_ID = 0 # Masks for flags component of FCGI_BEGIN_REQUEST FCGI_KEEP_CONN = 1 # Values for role component of FCGI_BEGIN_REQUEST FCGI_RESPONDER = 1 ; FCGI_AUTHORIZER = 2 ; FCGI_FILTER = 3 # Values for protocolStatus component of FCGI_END_REQUEST FCGI_REQUEST_COMPLETE = 0 # Request completed nicely FCGI_CANT_MPX_CONN = 1 # This app can't multiplex FCGI_OVERLOADED = 2 # New request rejected; too busy FCGI_UNKNOWN_ROLE = 3 # Role value not known error = 'fcgi.error' #--------------------------------------------------------------------------- # The following function is used during debugging; it isn't called # anywhere at the moment def error(msg): "Append a string to /tmp/err" errf = open('/tmp/err', 'a+') errf.write(msg+'\n') errf.close() #--------------------------------------------------------------------------- class record: "Class representing FastCGI records" def __init__(self): self.version = FCGI_VERSION_1 self.recType = FCGI_UNKNOWN_TYPE self.reqId = FCGI_NULL_REQUEST_ID self.content = "" #---------------------------------------- def readRecord(self, sock, unpack=struct.unpack): (self.version, self.recType, self.reqId, contentLength, paddingLength) = unpack(">BBHHBx", sock.recv(8)) content = "" while len(content) < contentLength: content = content + sock.recv(contentLength - len(content)) self.content = content if paddingLength != 0: padding = sock.recv(paddingLength) # Parse the content information if self.recType == FCGI_BEGIN_REQUEST: (self.role, self.flags) = unpack(">HB", content[:3]) elif self.recType == FCGI_UNKNOWN_TYPE: self.unknownType = ord(content[0]) elif self.recType == FCGI_GET_VALUES or self.recType == FCGI_PARAMS: self.values = {} pos = 0 while pos < len(content): name, value, pos = readPair(content, pos) self.values[name] = value elif self.recType == FCGI_END_REQUEST: (self.appStatus, self.protocolStatus) = unpack(">IB", content[0:5]) #---------------------------------------- def writeRecord(self, sock, pack=struct.pack): content = self.content if self.recType == FCGI_BEGIN_REQUEST: content = pack(">HBxxxxx", self.role, self.flags) elif self.recType == FCGI_UNKNOWN_TYPE: content = pack(">Bxxxxxx", self.unknownType) elif self.recType == FCGI_GET_VALUES or self.recType == FCGI_PARAMS: content = "" for i in self.values.keys(): content = content + writePair(i, self.values[i]) elif self.recType == FCGI_END_REQUEST: content = pack(">IBxxx", self.appStatus, self.protocolStatus) cLen = len(content) eLen = (cLen + 7) & (0xFFFF - 7) # align to an 8-byte boundary padLen = eLen - cLen hdr = pack(">BBHHBx", self.version, self.recType, self.reqId, cLen, padLen) ##debug.write('Sending fcgi record: %s\n' % repr(content[:50]) ) sock.send(hdr + content + padLen*'\000') #--------------------------------------------------------------------------- _lowbits = ~(1L << 31) # everything but the 31st bit def readPair(s, pos): nameLen = ord(s[pos]) ; pos = pos+1 if nameLen & 128: pos = pos + 3 nameLen = int(struct.unpack(">I", s[pos-4:pos])[0] & _lowbits) valueLen = ord(s[pos]) ; pos = pos+1 if valueLen & 128: pos = pos + 3 valueLen = int(struct.unpack(">I", s[pos-4:pos])[0] & _lowbits) return ( s[pos:pos+nameLen], s[pos+nameLen:pos+nameLen+valueLen], pos+nameLen+valueLen ) #--------------------------------------------------------------------------- _highbit = (1L << 31) def writePair(name, value): l = len(name) if l < 128: s = chr(l) else: s = struct.pack(">I", l | _highbit) l = len(value) if l < 128: s = s + chr(l) else: s = s + struct.pack(">I", l | _highbit) return s + name + value #--------------------------------------------------------------------------- def HandleManTypes(r, conn): if r.recType == FCGI_GET_VALUES: r.recType = FCGI_GET_VALUES_RESULT v = {} vars = {'FCGI_MAX_CONNS' : FCGI_MAX_CONNS, 'FCGI_MAX_REQS' : FCGI_MAX_REQS, 'FCGI_MPXS_CONNS': FCGI_MPXS_CONNS} for i in r.values.keys(): if i in vars: v[i] = vars[i] r.values = vars r.writeRecord(conn) #--------------------------------------------------------------------------- #--------------------------------------------------------------------------- _isFCGI = 1 # assume it is until we find out for sure def isFCGI(): return _isFCGI #--------------------------------------------------------------------------- _init = None _sock = None class FCGI: def __init__(self): self.haveFinished = 0 if _init == None: _startup() if not _isFCGI: self.haveFinished = 1 self.inp = sys.__stdin__ self.out = sys.__stdout__ self.err = sys.__stderr__ self.env = os.environ return if 'FCGI_WEB_SERVER_ADDRS' in os.environ: good_addrs = string.split(os.environ['FCGI_WEB_SERVER_ADDRS'], ',') good_addrs = map(string.strip, good_addrs) # Remove whitespace else: good_addrs = None self.conn, addr = _sock.accept() stdin, data = "", "" self.env = {} self.requestId = 0 remaining = 1 # Check if the connection is from a legal address if good_addrs != None and addr not in good_addrs: raise error, 'Connection from invalid server!' while remaining: r = record() r.readRecord(self.conn) if r.recType in ManagementTypes: HandleManTypes(r, self.conn) elif r.reqId == 0: # Oh, poopy. It's a management record of an unknown # type. Signal the error. r2 = record() r2.recType = FCGI_UNKNOWN_TYPE r2.unknownType = r.recType r2.writeRecord(self.conn) continue # Charge onwards # Ignore requests that aren't active elif r.reqId != self.requestId and r.recType != FCGI_BEGIN_REQUEST: continue # If we're already doing a request, ignore further BEGIN_REQUESTs elif r.recType == FCGI_BEGIN_REQUEST and self.requestId != 0: continue # Begin a new request if r.recType == FCGI_BEGIN_REQUEST: self.requestId = r.reqId if r.role == FCGI_AUTHORIZER: remaining = 1 elif r.role == FCGI_RESPONDER: remaining = 2 elif r.role == FCGI_FILTER: remaining = 3 elif r.recType == FCGI_PARAMS: if r.content == "": remaining = remaining-1 else: for i in r.values.keys(): self.env[i] = r.values[i] elif r.recType == FCGI_STDIN: if r.content == "": remaining = remaining-1 else: stdin = stdin+r.content elif r.recType == FCGI_DATA: if r.content == "": remaining = remaining-1 else: data = data+r.content # end of while remaining: self.inp = StringIO(stdin) self.err = StringIO() self.out = StringIO() self.data = StringIO(data) def __del__(self): self.Finish() def Finish(self, status=0): if not self.haveFinished: self.haveFinished = 1 self.err.seek(0,0) self.out.seek(0,0) ##global debug ##debug = open("/tmp/quixote-debug.log", "a+") ##debug.write("fcgi.FCGI.Finish():\n") r = record() r.recType = FCGI_STDERR r.reqId = self.requestId data = self.err.read() ##debug.write(" sending stderr (%s)\n" % `self.err`) ##debug.write(" data = %s\n" % `data`) while data: chunk, data = self.getNextChunk(data) ##debug.write(" chunk, data = %s, %s\n" % (`chunk`, `data`)) r.content = chunk r.writeRecord(self.conn) r.content = "" r.writeRecord(self.conn) # Terminate stream r.recType = FCGI_STDOUT data = self.out.read() ##debug.write(" sending stdout (%s)\n" % `self.out`) ##debug.write(" data = %s\n" % `data`) while data: chunk, data = self.getNextChunk(data) r.content = chunk r.writeRecord(self.conn) r.content = "" r.writeRecord(self.conn) # Terminate stream r = record() r.recType = FCGI_END_REQUEST r.reqId = self.requestId r.appStatus = status r.protocolStatus = FCGI_REQUEST_COMPLETE r.writeRecord(self.conn) self.conn.close() #debug.close() def getFieldStorage(self): method = 'GET' if 'REQUEST_METHOD' in self.env: method = string.upper(self.env['REQUEST_METHOD']) if method == 'GET': return cgi.FieldStorage(environ=self.env, keep_blank_values=1) else: return cgi.FieldStorage(fp=self.inp, environ=self.env, keep_blank_values=1) def getNextChunk(self, data): chunk = data[:8192] data = data[8192:] return chunk, data Accept = FCGI # alias for backwards compatibility #--------------------------------------------------------------------------- def _startup(): global _isFCGI, _init, _sock # This function won't work on Windows at all. if sys.platform[:3] == 'win': _isFCGI = 0 return _init = 1 try: s = socket.fromfd(sys.stdin.fileno(), socket.AF_INET, socket.SOCK_STREAM) s.getpeername() except socket.error, (err, errmsg): if err != errno.ENOTCONN: # must be a non-fastCGI environment _isFCGI = 0 return _sock = s #--------------------------------------------------------------------------- def _test(): counter = 0 try: while isFCGI(): req = Accept() counter = counter+1 try: fs = req.getFieldStorage() size = string.atoi(fs['size'].value) doc = ['*' * size] except: doc = ['' 'FCGI TestApp' '\n\n'] doc.append('

FCGI TestApp

') doc.append('request count = %d
' % counter) doc.append('pid = %s
' % os.getpid()) if 'CONTENT_LENGTH' in req.env: cl = string.atoi(req.env['CONTENT_LENGTH']) doc.append('
POST data (%s):

' % cl)
                    keys = fs.keys()
                    keys.sort()
                    for k in keys:
                        val = fs[k]
                        if type(val) == type([]):
                            doc.append('    %-15s :  %s\n'
                                       % (k, val))
                        else:
                            doc.append('    %-15s :  %s\n'
                                       % (k, val.value))
                    doc.append('
') doc.append('


')
                keys = req.env.keys()
                keys.sort()
                for k in keys:
                    doc.append('%-20s :  %s\n' % (k, req.env[k]))
                doc.append('\n


\n') doc.append('\n') doc = string.join(doc, '') req.out.write('Content-length: %s\r\n' 'Content-type: text/html\r\n' 'Cache-Control: no-cache\r\n' '\r\n' % len(doc)) req.out.write(doc) req.Finish() except: import traceback f = open('traceback', 'w') traceback.print_exc( file = f ) # f.write('%s' % doc) if __name__ == '__main__': #import pdb #pdb.run('_test()') _test() Quixote-2.7b2/quixote/server/cgi_server.py0000775000175000017500000000122111114154100017063 0ustar nasnas#!/usr/bin/env python import sys import os def run(create_publisher): if sys.platform == "win32": # on Windows, stdin and stdout are in text mode by default import msvcrt msvcrt.setmode(sys.__stdin__.fileno(), os.O_BINARY) msvcrt.setmode(sys.__stdout__.fileno(), os.O_BINARY) publisher = create_publisher() response = publisher.process(sys.__stdin__, os.environ) try: response.write(sys.__stdout__) except IOError, err: publisher.log("IOError while sending response ignored: %s" % err) if __name__ == '__main__': from quixote.demo import create_publisher run(create_publisher) Quixote-2.7b2/quixote/server/fastcgi_server.py0000775000175000017500000000120311114154100017741 0ustar nasnas#!/usr/bin/env python """Server for Quixote applications that use FastCGI. It should work for CGI too but the cgi_server module is preferred as it is more portable. """ from quixote.server import _fcgi def run(create_publisher): publisher = create_publisher() while _fcgi.isFCGI(): f = _fcgi.FCGI() response = publisher.process(f.inp, f.env) try: response.write(f.out) except IOError, err: publisher.log("IOError while sending response ignored: %s" % err) f.Finish() if __name__ == '__main__': from quixote.demo import create_publisher run(create_publisher) Quixote-2.7b2/quixote/server/medusa_server.py0000775000175000017500000000732711114154100017614 0ustar nasnas#!/usr/bin/env python """An HTTP handler for Medusa that publishes a Quixote application. """ import asyncore, rfc822, socket, urllib from StringIO import StringIO from medusa import http_server, xmlrpc_handler import quixote class StreamProducer: def __init__(self, chunks): self.chunks = chunks # a generator def more(self): try: return self.chunks.next() except StopIteration: return '' class QuixoteHandler: def __init__(self, publisher, server): self.publisher = publisher self.server = server def match(self, request): # Always match, since this is the only handler there is. return True def handle_request(self, request): msg = rfc822.Message(StringIO('\n'.join(request.header))) length = int(msg.get('Content-Length', '0')) if length: request.collector = xmlrpc_handler.collector(self, request) else: self.continue_request('', request) def continue_request(self, data, request): msg = rfc822.Message(StringIO('\n'.join(request.header))) remote_addr, remote_port = request.channel.addr if '#' in request.uri: # MSIE is buggy and sometimes includes fragments in URLs [request.uri, fragment] = request.uri.split('#', 1) if '?' in request.uri: [path, query_string] = request.uri.split('?', 1) else: path = request.uri query_string = '' path = urllib.unquote(path) server_port = str(self.server.port) http_host = msg.get("Host") if http_host: if ":" in http_host: server_name, server_port = http_host.split(":", 1) else: server_name = http_host else: server_name = (self.server.ip or socket.gethostbyaddr(socket.gethostname())[0]) environ = {'REQUEST_METHOD': request.command, 'ACCEPT_ENCODING': msg.get('Accept-encoding', ''), 'CONTENT_TYPE': msg.get('Content-type', ''), 'CONTENT_LENGTH': len(data), "GATEWAY_INTERFACE": "CGI/1.1", 'PATH_INFO': path, 'QUERY_STRING': query_string, 'REMOTE_ADDR': remote_addr, 'REMOTE_PORT': str(remote_port), 'REQUEST_URI': request.uri, 'SCRIPT_NAME': '', "SCRIPT_FILENAME": '', 'SERVER_NAME': server_name, 'SERVER_PORT': server_port, 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'Quixote/%s' % quixote.__version__, } for title, header in msg.items(): envname = 'HTTP_' + title.replace('-', '_').upper() environ[envname] = header stdin = StringIO(data) qresponse = self.publisher.process(stdin, environ) # Copy headers from Quixote's HTTP response for name, value in qresponse.generate_headers(): # XXX Medusa's HTTP request is buggy, and only allows unique # headers. request[name] = value request.response(qresponse.status_code) request.push(StreamProducer(qresponse.generate_body_chunks())) request.done() def run(create_publisher, host='', port=80): """Runs a Medusa HTTP server that publishes a Quixote application. """ server = http_server.http_server(host, port) publisher = create_publisher() handler = QuixoteHandler(publisher, server) server.install_handler(handler) asyncore.loop() if __name__ == '__main__': from quixote.server.util import main main(run) Quixote-2.7b2/quixote/server/mod_python_handler.py0000664000175000017500000000575211114154100020622 0ustar nasnas""" This needs testing. mod_python configuration ------------------------ mod_python is an Apache module for embedding a Python interpreter into the Apache server. To use mod_python as the interface layer between Apache and Quixote, add something like this to your httpd.conf:: LoadModule python_module /usr/lib/apache/1.3/mod_python.so SetHandler python-program PythonHandler quixote.server.mod_python_handler PythonOption quixote-publisher-factory quixote.demo.create_publisher PythonInterpreter quixote.demo PythonDebug On This will attach URLs starting with ``/qdemo`` to the Quixote demo. When you use mod_python, there's no need for rewrite rules (because of the pattern in the ``LocationMatch`` directive), and no need for a driver script. mod_python support was contributed to Quixote (1) by Erno Kuusela and the Quixote 2 port comes from Clint. """ import sys from mod_python import apache from quixote.publish import Publisher from quixote.util import import_object class ErrorLog: def __init__(self, publisher): self.publisher = publisher def write(self, msg): self.publisher.log(msg) def close(self): pass class ModPythonPublisher(Publisher): def __init__(self, package, **kwargs): Publisher.__init__(self, package, **kwargs) # may be overwritten self.logger.error_log = self.__error_log = ErrorLog(self) self.__apache_request = None def log(self, msg): if self.logger.error_log is self.__error_log: try: self.__apache_request.log_error(msg) except AttributeError: apache.log_error(msg) else: Publisher.log(self, msg) def publish_modpython(self, req): """publish_modpython() -> None Entry point from mod_python. """ self.__apache_request = req try: self.publish(apache.CGIStdin(req), apache.CGIStdout(req), sys.stderr, apache.build_cgi_env(req)) return apache.OK finally: self.__apache_request = None name2publisher = {} def run(publisher, req): response = publisher.process(apache.CGIStdin(req), apache.build_cgi_env(req)) try: response.write(apache.CGIStdout(req)) except IOError, err: publisher.log("IOError while sending response ignored: %s" % err) return apache.OK def handler(req): opts = req.get_options() try: factory = opts['quixote-publisher-factory'] except KeyError: apache.log_error('quixote-publisher-factory setting required') return apache.HTTP_INTERNAL_SERVER_ERROR pub = name2publisher.get(factory) if pub is None: factory_fcn = import_object(factory) pub = factory_fcn() name2publisher[factory] = pub return run(pub, req) Quixote-2.7b2/quixote/server/scgi_server.py0000775000175000017500000000575011272201223017265 0ustar nasnas#!/usr/bin/env python """A SCGI server that uses Quixote to publish dynamic content. """ from scgi import scgi_server class QuixoteHandler(scgi_server.SCGIHandler): def __init__(self, parent_fd, create_publisher, script_name=None): scgi_server.SCGIHandler.__init__(self, parent_fd) self.publisher = create_publisher() self.script_name = script_name def handle_connection(self, conn): input = conn.makefile("r") output = conn.makefile("w") env = self.read_env(input) if self.script_name is not None: # mod_scgi doesn't know SCRIPT_NAME :-( prefix = self.script_name path = env['SCRIPT_NAME'] assert path[:len(prefix)] == prefix, ( "path %r doesn't start with script_name %r" % (path, prefix)) env['SCRIPT_NAME'] = prefix env['PATH_INFO'] = path[len(prefix):] + env.get('PATH_INFO', '') response = self.publisher.process(input, env) try: response.write(output) input.close() output.close() conn.close() except IOError, err: self.publisher.log("IOError while sending response " "ignored: %s" % err) def run(create_publisher, host='localhost', port=3000, script_name=None, max_children=5): def create_handler(parent_fd): return QuixoteHandler(parent_fd, create_publisher, script_name) s = scgi_server.SCGIServer(create_handler, host=host, port=port, max_children=max_children) s.serve() def main(): from optparse import OptionParser from quixote.util import import_object parser = OptionParser() parser.set_description(run.__doc__) default_host = 'localhost' parser.add_option( '--host', dest="host", default=default_host, type="string", help="Host interface to listen on. (default=%s)" % default_host) default_port = 3000 parser.add_option( '--port', dest="port", default=default_port, type="int", help="Port to listen on. (default=%s)" % default_port) default_maxchild = 5 parser.add_option( '--max-children', dest="maxchild", default=default_maxchild, type="string", help="Maximum number of children to spawn. (default=%s)" % default_maxchild) parser.add_option( '--script-name', dest="script_name", default=None, type="string", help="Value of SCRIPT_NAME (only needed if using mod_scgi)") default_factory = 'quixote.demo.create_publisher' parser.add_option( '--factory', dest="factory", default=default_factory, help="Path to factory function to create the site Publisher. " "(default=%s)" % default_factory) (options, args) = parser.parse_args() run(import_object(options.factory), host=options.host, port=options.port, script_name=options.script_name, max_children=options.maxchild) if __name__ == '__main__': main() Quixote-2.7b2/quixote/server/simple_server.py0000775000175000017500000001063411114154100017622 0ustar nasnas#!/usr/bin/env python """A simple, single threaded, synchronous HTTP server. """ import sys from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import urllib import quixote from quixote import get_publisher from quixote.util import import_object class HTTPRequestHandler(BaseHTTPRequestHandler): required_cgi_environment = {} def get_cgi_env(self, method): env = dict( SERVER_SOFTWARE="Quixote/%s" % quixote.__version__, SERVER_NAME=self.server.server_name, GATEWAY_INTERFACE='CGI/1.1', SERVER_PROTOCOL=self.protocol_version, SERVER_PORT=str(self.server.server_port), REQUEST_METHOD=method, REMOTE_ADDR=self.client_address[0], SCRIPT_NAME='') if '?' in self.path: env['PATH_INFO'], env['QUERY_STRING'] = self.path.split('?', 1) else: env['PATH_INFO'] = self.path env['PATH_INFO'] = urllib.unquote(env['PATH_INFO']) if self.headers.typeheader is None: env['CONTENT_TYPE'] = self.headers.type else: env['CONTENT_TYPE'] = self.headers.typeheader env['CONTENT_LENGTH'] = self.headers.getheader('content-length') or "0" for name, value in self.headers.items(): header_name = 'HTTP_' + name.upper().replace('-', '_') env[header_name] = value accept = [] for line in self.headers.getallmatchingheaders('accept'): if line[:1] in "\t\n\r ": accept.append(line.strip()) else: accept = accept + line[7:].split(',') env['HTTP_ACCEPT'] = ','.join(accept) co = filter(None, self.headers.getheaders('cookie')) if co: env['HTTP_COOKIE'] = ', '.join(co) env.update(self.required_cgi_environment) return env def process(self, env, include_body=True): response = get_publisher().process(self.rfile, env) try: self.send_response(response.get_status_code(), response.get_reason_phrase()) response.write(self.wfile, include_status=False, include_body=include_body) except IOError, err: print "IOError while sending response ignored: %s" % err def do_POST(self): return self.process(self.get_cgi_env('POST')) def do_GET(self): return self.process(self.get_cgi_env('GET')) def do_HEAD(self): return self.process(self.get_cgi_env('HEAD'), include_body=False) def send_response(self, code, message=None): """ Copied, with regret, from BaseHTTPRequestHandler, except that the line that adds the 'Date' header is removed to avoid duplicating the one that Quixote adds. """ self.log_request(code) if message is None: if code in self.responses: message = self.responses[code][0] else: message = '' if self.request_version != 'HTTP/0.9': self.wfile.write("%s %d %s\r\n" % (self.protocol_version, code, message)) self.send_header('Server', self.version_string()) def run(create_publisher, host='', port=80, https=False): """Runs a simple, single threaded, synchronous HTTP server that publishes a Quixote application. """ if https: HTTPRequestHandler.required_cgi_environment['HTTPS'] = 'on' httpd = HTTPServer((host, port), HTTPRequestHandler) def handle_error(request, client_address): HTTPServer.handle_error(httpd, request, client_address) if sys.exc_info()[0] is SystemExit: raise httpd.handle_error = handle_error publisher = create_publisher() try: httpd.serve_forever() finally: httpd.server_close() if __name__ == '__main__': from quixote.server.util import get_server_parser parser = get_server_parser(run.__doc__) parser.add_option( '--https', dest="https", default=False, action="store_true", help=("Force the scheme for all requests to be https. " "Not that this is for running the simple server " "through a proxy or tunnel that provides real SSL " "support. The simple server itself does not. ")) (options, args) = parser.parse_args() run(import_object(options.factory), host=options.host, port=options.port, https=options.https) Quixote-2.7b2/quixote/server/twisted_server.py0000775000175000017500000001141711114154100020014 0ustar nasnas#!/usr/bin/env python """An HTTP server for Twisted that publishes a Quixote application. """ import urllib from twisted.protocols import http from twisted.web import server from twisted.python import threadable from twisted.internet import reactor class QuixoteFactory(http.HTTPFactory): def __init__(self, publisher): self.publisher = publisher http.HTTPFactory.__init__(self, None) def buildProtocol(self, addr): protocol = http.HTTPFactory.buildProtocol(self, addr) protocol.requestFactory = QuixoteRequest return protocol class QuixoteRequest(server.Request): def process(self): environ = self.create_environment() # this seek is important, it doesn't work without it (it doesn't # matter for GETs, but POSTs will not work properly without it.) self.content.seek(0, 0) qxresponse = self.channel.factory.publisher.process(self.content, environ) self.setResponseCode(qxresponse.status_code) for name, value in qxresponse.generate_headers(): if name != 'Set-Cookie': self.setHeader(name, value) # Cookies get special treatment since it seems Twisted cannot handle # multiple Set-Cookie headers. for name, attrs in qxresponse.cookies.items(): attrs = attrs.copy() value = attrs.pop('value') self.addCookie(name, value, **attrs) QuixoteProducer(qxresponse, self) def create_environment(self): """ Borrowed heavily from twisted.web.twcgi """ # Twisted doesn't decode the path for us, so let's do it here. if '%' in self.path: self.path = urllib.unquote(self.path) serverName = self.getRequestHostname().split(':')[0] env = {"SERVER_SOFTWARE": server.version, "SERVER_NAME": serverName, "GATEWAY_INTERFACE": "CGI/1.1", "SERVER_PROTOCOL": self.clientproto, "SERVER_PORT": str(self.getHost()[2]), "REQUEST_METHOD": self.method, "SCRIPT_NAME": '', "SCRIPT_FILENAME": '', "REQUEST_URI": self.uri, "HTTPS": (self.isSecure() and 'on') or 'off', 'SERVER_PROTOCOL': 'HTTP/1.1', } for env_var, header in [('ACCEPT_ENCODING', 'Accept-encoding'), ('CONTENT_TYPE', 'Content-type'), ('HTTP_COOKIE', 'Cookie'), ('HTTP_REFERER', 'Referer'), ('HTTP_USER_AGENT', 'User-agent')]: value = self.getHeader(header) if value is not None: env[env_var] = value client = self.getClient() if client is not None: env['REMOTE_HOST'] = client ip = self.getClientIP() if ip is not None: env['REMOTE_ADDR'] = ip _, _, remote_port = self.transport.getPeer() env['REMOTE_PORT'] = remote_port env["PATH_INFO"] = self.path qindex = self.uri.find('?') if qindex != -1: env['QUERY_STRING'] = self.uri[qindex+1:] else: env['QUERY_STRING'] = '' # Propogate HTTP headers for title, header in self.getAllHeaders().items(): envname = title.replace('-', '_').upper() if title not in ('content-type', 'content-length'): envname = "HTTP_" + envname env[envname] = header return env class QuixoteProducer: """ Produce the Quixote response for twisted. """ def __init__(self, qxresponse, request): self.request = request self.size = qxresponse.get_content_length() self.stream = qxresponse.generate_body_chunks() request.registerProducer(self, 0) def resumeProducing(self): if self.request: try: chunk = self.stream.next() except StopIteration: self.request.unregisterProducer() self.request.finish() self.request = None else: self.request.write(chunk) def pauseProducing(self): pass def stopProducing(self): self.request = None synchronized = ['resumeProducing', 'stopProducing'] threadable.synchronize(QuixoteProducer) def run(create_publisher, host='', port=80): """Runs a Twisted HTTP server server that publishes a Quixote application.""" publisher = create_publisher() factory = QuixoteFactory(publisher) reactor.listenTCP(port, factory, interface=host) reactor.run() if __name__ == '__main__': from quixote.server.util import main main(run) Quixote-2.7b2/quixote/server/util.py0000664000175000017500000000203611114154100015712 0ustar nasnas"""Miscellaneous utility functions shared by servers. """ from optparse import OptionParser from quixote.util import import_object def get_server_parser(doc): parser = OptionParser() parser.set_description(doc) default_host = 'localhost' parser.add_option( '--host', dest="host", default=default_host, type="string", help="Host interface to listen on. (default=%s)" % default_host) default_port = 8080 parser.add_option( '--port', dest="port", default=default_port, type="int", help="Port to listen on. (default=%s)" % default_port) default_factory = 'quixote.demo.create_publisher' parser.add_option( '--factory', dest="factory", default=default_factory, help="Path to factory function to create the site Publisher. " "(default=%s)" % default_factory) return parser def main(run): parser = get_server_parser(run.__doc__) (options, args) = parser.parse_args() run(import_object(options.factory), host=options.host, port=options.port) Quixote-2.7b2/quixote/test/0000775000175000017500000000000011326377244014060 5ustar nasnasQuixote-2.7b2/quixote/test/__init__.py0000664000175000017500000000005711114154100016146 0ustar nasnas # Empty file to make this directory a package Quixote-2.7b2/quixote/test/ua_test.py0000664000175000017500000000150411114154100016051 0ustar nasnas#!/usr/bin/env python # Test Quixote's ability to parse the "User-Agent" header, ie. # the 'guess_browser_version()' method of HTTPRequest. # # Reads User-Agent strings on stdin, and writes Quixote's interpretation # of each on stdout. This is *not* an automated test! import sys, os from copy import copy from quixote.http_request import HTTPRequest if __name__ == '__main__': env = copy(os.environ) file = sys.stdin while 1: line = file.readline() if not line: break if line[-1] == "\n": line = line[:-1] env["HTTP_USER_AGENT"] = line req = HTTPRequest(None, env) (name, version) = req.guess_browser_version() if name is None: print "%s -> ???" % line else: print "%s -> (%s, %s)" % (line, name, version) Quixote-2.7b2/quixote/test/utest_request.py0000775000175000017500000000332511114154100017327 0ustar nasnasfrom sancho.utest import UTest from quixote.http_request import parse_cookies class ParseCookiesTest (UTest): def check_basic(self): assert parse_cookies('a') == {'a': ''} assert parse_cookies('a = ') == {'a': ''} assert parse_cookies('a = ""') == {'a': ''} assert parse_cookies(r'a = "\""') == {'a': '"'} assert parse_cookies('a, b; c') == {'a': '', 'b': '', 'c': ''} assert parse_cookies('a, b=1') == {'a': '', 'b': '1'} assert parse_cookies('a = ";, \t";') == {'a': ';, \t'} def check_rfc2109_example(self): s = ('$Version="1"; Customer="WILE_E_COYOTE"; $Path="/acme"; ' 'Part_Number="Rocket_Launcher_0001"; $Path="/acme"') result = {'Customer': 'WILE_E_COYOTE', 'Part_Number': 'Rocket_Launcher_0001', } assert parse_cookies(s) == result def check_other(self): s = 'PREF=ID=0a06b1:TM=108:LM=1069:C2COFF=1:S=ETXrcU' result = {'PREF': 'ID=0a06b1:TM=108:LM=1069:C2COFF=1:S=ETXrcU'} assert parse_cookies(s) == result s = 'pageColor=White; pageWidth=990; fontSize=12; fontFace=1; E=E' assert parse_cookies(s) == {'pageColor': 'White', 'pageWidth': '990', 'fontSize': '12', 'fontFace': '1', 'E': 'E'} s = 'userid="joe"; QX_session="58a3ced39dcd0d"' assert parse_cookies(s) == {'userid': 'joe', 'QX_session': '58a3ced39dcd0d'} def check_invalid(self): parse_cookies('a="123') parse_cookies('a=123"') if __name__ == "__main__": ParseCookiesTest() Quixote-2.7b2/quixote/__init__.py0000664000175000017500000000173411326376747015226 0ustar nasnas"""Quixote A small and flexible Python web application framework. """ __version__ = '2.7b2' # These are frequently needed by Quixote applications. from quixote.publish import \ get_publisher, get_request, get_response, get_path, redirect, \ get_session, get_session_manager, get_user, get_field, get_cookie, \ get_wsgi_app, cleanup # This is the default charset used by the HTTPRequest, HTTPResponse, # DefaultLogger, and sendmail components. DEFAULT_CHARSET = 'utf-8' def enable_ptl(): """ Installs the import hooks needed to import PTL modules. This must be done explicitly because not all Quixote applications need to use PTL, and import hooks are deep magic that can cause all sorts of mischief and deeply confuse innocent bystanders. Thus, we avoid invoking them behind the programmer's back. One known problem is that, if you use ZODB, you must import ZODB before calling this function. """ import quixote.ptl.install Quixote-2.7b2/quixote/config.py0000664000175000017500000001515711210626632014717 0ustar nasnas""" Quixote configuration information. This module provides both the default configuration values, and some code that Quixote uses for dealing with configuration info. You should not edit the configuration values in this file, since your edits will be lost if you upgrade to a newer Quixote version in the future. However, this is the canonical source of information about Quixote configuration variables, and editing the defaults here is harmless if you're just playing around and don't care what happens in the future. """ # Note that the default values here are geared towards a production # environment, preferring security and performance over verbosity and # debug-ability. If you just want to get a Quixote application # up-and-running in a production environment, these settings are mostly # right; all you really need to customize are ERROR_EMAIL, and ERROR_LOG. # If you need to test/debug/develop a Quixote application, though, you'll # probably want to also change DISPLAY_EXCEPTIONS. # Again, you shouldn't edit this file unless you don't care what happens # in the future (in particular, an upgrade to Quixote would clobber your # edits). # E-mail address to send application errors to; None to send no mail at # all. This should probably be the email address of your web # administrator. ERROR_EMAIL = None #ERROR_EMAIL = 'webmaster@example.com' # Filename for writing the Quixote access log; None for no access log. ACCESS_LOG = None #ACCESS_LOG = "/www/log/quixote-access.log" # Filename for logging error messages and debugging output; if None, # everything will be sent to standard error (normally ending up in the # Web server's error log file. ERROR_LOG = None # Controls what's done when uncaught exceptions occur. If set to # 'plain', the traceback will be returned to the browser in addition # to being logged, If set to 'html' and the cgitb module is installed, # a more elaborate display will be returned to the browser, showing # the local variables and a few lines of context for each level of the # traceback. If set to None, a generic error display, containing no # information about the traceback, will be used. DISPLAY_EXCEPTIONS = None # Compress large pages using gzip if the client accepts that encoding. COMPRESS_PAGES = False # If true, then a cryptographically secure token will be inserted into forms # as a hidden field. The token will be checked when the form is submitted. # This prevents cross-site request forgeries (CSRF). It is off by default # since it doesn't work if sessions are not persistent across requests. FORM_TOKENS = False # Session-related variables # ========================= # Name of the cookie that will hold the session ID string. SESSION_COOKIE_NAME = "QX_session" # Domain and path to which the session cookie is restricted. Leaving # these undefined is fine. Quixote does not have a default "domain" # option, meaning the session cookie will only be sent to the # originating server. If you don't set the cookie path, Quixote will # use your application's root URL (ie. SCRIPT_NAME in a CGI-like # environment), meaning the session cookie will be sent to all URLs # controlled by your application, but no other. SESSION_COOKIE_DOMAIN = None # eg. ".example.com" SESSION_COOKIE_PATH = None # eg. "/" SESSION_COOKIE_SECURE = False SESSION_COOKIE_HTTPONLY = False # Mail-related variables # ====================== # These are only used by the quixote.sendmail module, which is # provided for use by Quixote applications that need to send # e-mail. This is a common task for web apps, but by no means # universal. # # E-mail addresses can be specified either as a lone string # containing a bare e-mail address ("addr-spec" in the RFC 822 # grammar), or as an (address, real_name) tuple. # MAIL_FROM is used as the default for the "From" header and the SMTP # sender for all outgoing e-mail. If you don't set it, your application # will crash the first time it tries to send e-mail without an explicit # "From" address. MAIL_FROM = None # eg. "webmaster@example.com" # or ("webmaster@example.com", "Example Webmaster") # E-mail is sent by connecting to an SMTP server on MAIL_SERVER. This # server must be configured to relay outgoing e-mail from the current # host (ie., the host where your Quixote application runs, most likely # your web server) to anywhere on the Internet. If you don't know what # this means, talk to your system administrator. MAIL_SERVER = "localhost" # If MAIL_DEBUG_ADDR is set, then all e-mail will actually be sent to # this address rather than the intended recipients. This should be a # single, bare e-mail address. MAIL_DEBUG_ADDR = None # eg. "developers@example.com" # -- End config variables ---------------------------------------------- # (no user serviceable parts after this point) class Config: """Holds all Quixote configuration variables -- see above for documentation of them. The naming convention is simple: downcase the above variables to get the names of instance attributes of this class. """ config_vars = [ 'error_email', 'access_log', 'display_exceptions', 'error_log', 'compress_pages', 'form_tokens', 'session_cookie_domain', 'session_cookie_name', 'session_cookie_path', 'session_cookie_secure', 'session_cookie_httponly', 'mail_from', 'mail_server', 'mail_debug_addr', ] def __init__(self, **kwargs): self.set_from_dict(globals()) # set defaults for name, value in kwargs.items(): if name not in self.config_vars: raise ValueError('unknown config variable %r' % name) setattr(self, name, value) def set_from_dict(self, config_vars): for name, value in config_vars.items(): if name.isupper(): name = name.lower() if name not in self.config_vars: raise ValueError('unknown config variable %r' % name) setattr(self, name, value) def read_file(self, filename): """Read configuration from a file. Any variables already defined in this Config instance, but not in the file, are unchanged, so you can use this to build up a configuration by accumulating data from several config files. """ # The config file is Python code -- makes life easy. config_vars = {} try: execfile(filename, config_vars) except IOError, exc: if exc.filename is None: # arg! execfile() loses filename exc.filename = filename raise exc self.set_from_dict(config_vars) Quixote-2.7b2/quixote/directory.py0000664000175000017500000000673611307241201015451 0ustar nasnas"""Logic for traversing directory objects and generating output. """ import quixote from quixote.errors import TraversalError class Directory(object): """ Instance attributes: none """ # A list containing strings or 2-tuples of strings that map external # names to internal names. Note that the empty string will be # implicitly mapped to '_q_index'. _q_exports = [] def _q_translate(self, component): """(component : string) -> string | None Translate a path component into a Python identifier. Returning None signifies that the component does not exist. """ if component in self._q_exports: if component == '': return '_q_index' # implicit mapping else: return component else: # check for an explicit external to internal mapping for value in self._q_exports: if isinstance(value, tuple): if value[0] == component: return value[1] else: return None def _q_lookup(self, component): """(component : string) -> object Lookup a path component and return the corresponding object (usually a Directory, a method or a string). Returning None signals that the component does not exist. """ return None def _q_traverse(self, path): """(path: [string]) -> object Traverse a path and return the result. """ assert len(path) > 0 component = path[0] path = path[1:] name = self._q_translate(component) if name is not None: obj = getattr(self, name) else: obj = self._q_lookup(component) if obj is None: raise TraversalError(private_msg=('directory %r has no component ' '%r' % (self, component))) if path: if hasattr(obj, '_q_traverse'): return obj._q_traverse(path) else: raise TraversalError elif hasattr(obj, '__call__'): return obj() else: return obj def __call__(self): if "" in self._q_exports and not quixote.get_request().form: # Fix missing trailing slash. path = quixote.get_path() print "Adding slash to: %r " % path return quixote.redirect(path + "/", permanent=True) else: raise TraversalError(private_msg=('directory %r is not ' 'callable' % self)) class AccessControlled(object): """ A mix-in class that calls the _q_access() method before traversing into the directory. """ def _q_access(self): pass def _q_traverse(self, path): self._q_access() return super(AccessControlled, self)._q_traverse(path) class Resolving(object): """ A mix-in class that provides the _q_resolve() method. _q_resolve() is called if a component name appears in the _q_exports list but is not an instance attribute. _q_resolve is expected to return the component object. """ def _q_resolve(self, name): return None def _q_translate(self, component): name = super(Resolving, self)._q_translate(component) if name is not None and not hasattr(self, name): obj = self._q_resolve(name) setattr(self, name, obj) return name Quixote-2.7b2/quixote/errors.py0000664000175000017500000001125111114154100014742 0ustar nasnas"""quixote.errors Exception classes used by Quixote """ from quixote.html import htmltext, htmlescape class PublishError(Exception): """PublishError exceptions are raised due to some problem with the data provided by the client and are raised during the publishing process. Quixote will abort the current request and return an error page to the client. public_msg should be a user-readable message that reveals no inner workings of your application; it will always be shown. private_msg will only be shown if the config option DISPLAY_EXCEPTIONS is true; Quixote uses this to give you more detail about why the error occurred. You might want to use it for similar, application-specific information. (DISPLAY_EXCEPTIONS should always be false in a production environment, since these details about the inner workings of your application could conceivably be useful to attackers.) The formatting done by the Quixote versions of these exceptions is very simple. Applications will probably wish to raise application specific subclasses which do more sophisticated formatting or provide a _q_except handler to format the exception. """ status_code = 400 # bad request title = "Publishing error" description = "no description" def __init__(self, public_msg=None, private_msg=None): self.public_msg = public_msg self.private_msg = private_msg # cleared if DISPLAY_EXCEPTIONS is false def __str__(self): return self.private_msg or self.public_msg or "???" def format(self): msg = htmlescape(self.title) if self.public_msg: msg = msg + ": " + self.public_msg if self.private_msg: msg = msg + ": " + self.private_msg return msg class TraversalError(PublishError): """ Raised when a client attempts to access a resource that does not exist or is otherwise unavailable to them (eg. a Python function not listed in its module's _q_exports list). path should be the path to the requested resource; if not supplied, the current request object will be fetched and its get_path() method called. """ status_code = 404 # not found title = "Page not found" description = ("The requested link does not exist on this site. If " "you arrived here by following a link from an external " "page, please inform that page's maintainer.") def __init__(self, public_msg=None, private_msg=None, path=None): PublishError.__init__(self, public_msg, private_msg) if path is None: import quixote path = quixote.get_request().get_path() self.path = path def format(self): msg = htmlescape(self.title) + ": " + self.path if self.public_msg: msg = msg + ": " + self.public_msg if self.private_msg: msg = msg + ": " + self.private_msg return msg class RequestError(PublishError): """ Raised when Quixote is unable to parse an HTTP request (or its CGI representation). This is a lower-level error than QueryError -- it either means that Quixote is not smart enough to handle the request being passed to it, or the user-agent is broken and/or malicious. """ status_code = 400 title = "Invalid request" description = "Unable to parse HTTP request." class QueryError(PublishError): """Should be raised if bad data was provided in the query part of a URL or in the content of a POST request. What constitutes bad data is solely application dependent (eg: letters in a form field when the application expects a number). """ status_code = 400 title = "Invalid query" description = ("An error occurred while handling your request. The " "query data provided as part of the request is invalid.") class AccessError(PublishError): """Should be raised if the client does not have access to the requested resource. Usually applications will raise this error from an _q_access method. """ status_code = 403 title = "Access denied" description = ("An error occurred while handling your request. " "Access to the requested resource was not permitted.") def format_publish_error(exc): """(exc : PublishError) -> string Format a PublishError exception as a web page. """ return htmltext("""\ Error: %s

%s

%s

""") % (exc.title, exc.description, exc.format()) Quixote-2.7b2/quixote/http_request.py0000664000175000017500000006617511326376512016216 0ustar nasnas"""quixote.http_request Provides the HTTPRequest class and related code for parsing HTTP requests, such as the Upload class. """ import re import string import os import tempfile import urllib import rfc822 from StringIO import StringIO import quixote from quixote.http_response import HTTPResponse from quixote.errors import RequestError # Various regexes for parsing specific bits of HTTP, all from RFC 2616. # These are needed by 'get_encoding()', to parse the "Accept-Encoding" # header. LWS is linear whitespace; the latter two assume that LWS # has been removed. _http_lws_re = re.compile(r"(\r\n)?[ \t]+") _http_list_re = re.compile(r",+") _http_encoding_re = re.compile(r"([^;]+)(;q=([\d.]+))?$") # These are needed by 'guess_browser_version()', for parsing the # "User-Agent" header. # token = 1* # CHAR = any 7-bit US ASCII character (0-127) # separators are ( ) < > @ , ; : \ " / [ ] ? = { } # # The user_agent RE is a simplification; it only looks for one "product", # possibly followed by a comment. _http_token_pat = r"[\w!#$%&'*+.^`|~-]+" _http_product_pat = r'(%s)(?:/(%s))?' % (_http_token_pat, _http_token_pat) _http_product_re = re.compile(_http_product_pat) _comment_delim_re = re.compile(r';\s*') def get_content_type(environ): ctype = environ.get("CONTENT_TYPE") if ctype: return ctype.split(";")[0] else: return None def _decode_string(s, charset): if charset == 'iso-8859-1' == quixote.DEFAULT_CHARSET: # To avoid breaking applications that are not Unicode-safe, return # a str instance in this case. return s try: return s.decode(charset) except LookupError: raise RequestError('unknown charset %r' % charset) except UnicodeDecodeError: raise RequestError('invalid %r encoded string' % charset) def parse_header(line): """Parse a Content-type like header. Return the main content-type and a dictionary of options. """ plist = map(lambda x: x.strip(), line.split(';')) key = plist.pop(0).lower() pdict = {} for p in plist: i = p.find('=') if i >= 0: name = p[:i].strip().lower() value = p[i+1:].strip() if len(value) >= 2 and value[0] == value[-1] == '"': value = value[1:-1] pdict[name] = value return key, pdict def parse_content_disposition(full_cdisp): (cdisp, cdisp_params) = parse_header(full_cdisp) name = cdisp_params.get('name') if not (cdisp == 'form-data' and name): raise RequestError('expected Content-Disposition: form-data ' 'with a "name" parameter: got %r' % full_cdisp) return (name, cdisp_params.get('filename')) def parse_query(qs, charset): """(qs: string) -> {key:string, string|[string]} Parse a query given as a string argument and return a dictionary. """ fields = {} for chunk in filter(None, qs.split('&')): if '=' not in chunk: name = chunk value = '' else: name, value = chunk.split('=', 1) name = urllib.unquote(name.replace('+', ' ')) value = urllib.unquote(value.replace('+', ' ')) name = _decode_string(name, charset) value = _decode_string(value, charset) _add_field_value(fields, name, value) return fields def _add_field_value(fields, name, value): if name in fields: values = fields[name] if not isinstance(values, list): fields[name] = values = [values] values.append(value) else: fields[name] = value class HTTPRequest: """ Model a single HTTP request and all associated data: environment variables, form variables, cookies, etc. To access environment variables associated with the request, use get_environ(): eg. request.get_environ('SERVER_PORT', 80). To access form variables, use get_field(), eg. request.get_field("name"). To access cookies, use get_cookie(). Various bits and pieces of the requested URL can be accessed with get_url(), get_path(), get_server() The HTTPResponse object corresponding to this request is available in the 'response' attribute. This is rarely needed: eg. to send an error response, you should raise one of the exceptions in errors.py; to send a redirect, you should use the quixote.redirect() function, which lets you specify relative URLs. However, if you need to tweak the response object in other ways, you can do so via 'response'. Just keep in mind that Quixote discards the original response object when handling an exception. """ DEFAULT_CHARSET = None # defaults to quixote.DEFAULT_CHARSET def __init__(self, stdin, environ): self.stdin = stdin self.environ = environ self.form = {} self.session = None self.charset = self.DEFAULT_CHARSET or quixote.DEFAULT_CHARSET self.response = HTTPResponse() # The strange treatment of SERVER_PORT_SECURE is because IIS # sets this environment variable to "0" for non-SSL requests # (most web servers -- well, Apache at least -- simply don't set # it in that case). if (environ.get('HTTPS', 'off').lower() in ('on', 'yes', '1') or environ.get('SERVER_PORT_SECURE', '0') != '0'): self.scheme = "https" else: self.scheme = "http" k = self.environ.get('HTTP_COOKIE', '') if k: self.cookies = parse_cookies(k) else: self.cookies = {} # IIS breaks PATH_INFO because it leaves in the path to # the script, so SCRIPT_NAME is "/cgi-bin/q.py" and PATH_INFO # is "/cgi-bin/q.py/foo/bar". The following code fixes # PATH_INFO to the expected value "/foo/bar". web_server = environ.get('SERVER_SOFTWARE', 'unknown') if web_server.find('Microsoft-IIS') != -1: script = environ['SCRIPT_NAME'] path = environ['PATH_INFO'] if path.startswith(script): path = path[len(script):] self.environ['PATH_INFO'] = path def process_inputs(self): query = self.get_query() if query: self.form.update(parse_query(query, self.charset)) length = self.environ.get('CONTENT_LENGTH') or "0" try: length = int(length) except ValueError: raise RequestError('invalid content-length header') ctype = self.environ.get("CONTENT_TYPE") if ctype: ctype, ctype_params = parse_header(ctype) if ctype == 'application/x-www-form-urlencoded': self._process_urlencoded(length, ctype_params) elif ctype == 'multipart/form-data': self._process_multipart(length, ctype_params) def _process_urlencoded(self, length, params): query = self.stdin.read(length) if len(query) != length: raise RequestError('unexpected end of request body') # Use the declared charset if it's provided (most browser's don't # provide it to avoid breaking old HTTP servers). charset = params.get('charset', self.charset) self.form.update(parse_query(query, charset)) def _process_multipart(self, length, params): boundary = params.get('boundary') if not boundary: raise RequestError('multipart/form-data missing boundary') charset = params.get('charset') mimeinput = MIMEInput(self.stdin, boundary, length) try: for line in mimeinput.readpart(): pass # discard lines up to first boundary while mimeinput.moreparts(): self._process_multipart_body(mimeinput, charset) except EOFError: raise RequestError('unexpected end of multipart/form-data') def _process_multipart_body(self, mimeinput, charset): headers = StringIO() lines = mimeinput.readpart() for line in lines: headers.write(line) if line == '\r\n': break headers.seek(0) headers = rfc822.Message(headers) ctype, ctype_params = parse_header(headers.get('content-type', '')) if ctype and 'charset' in ctype_params: charset = ctype_params['charset'] cdisp, cdisp_params = parse_header(headers.get('content-disposition', '')) if not cdisp: raise RequestError('expected Content-Disposition header') name = cdisp_params.get('name') filename = cdisp_params.get('filename') if not (cdisp == 'form-data' and name): raise RequestError('expected Content-Disposition: form-data' 'with a "name" parameter: got %r' % headers.get('content-disposition', '')) # FIXME: should really to handle Content-Transfer-Encoding and other # MIME complexity here. See RFC2048 for the full horror story. if filename: # it might be large file upload so use a temporary file upload = Upload(filename, ctype, charset) upload.receive(lines) _add_field_value(self.form, name, upload) else: value = _decode_string(''.join(lines), charset or self.charset) _add_field_value(self.form, name, value) def get_header(self, name, default=None): """get_header(name : string, default : string = None) -> string Return the named HTTP header, or an optional default argument (or None) if the header is not found. Note that both original and CGI-ified header names are recognized, e.g. 'Content-Type', 'CONTENT_TYPE' and 'HTTP_CONTENT_TYPE' should all return the Content-Type header, if available. """ environ = self.environ name = name.replace("-", "_").upper() val = environ.get(name) if val is not None: return val if name[:5] != 'HTTP_': name = 'HTTP_' + name return environ.get(name, default) def get_cookie(self, cookie_name, default=None): return self.cookies.get(cookie_name, default) def get_cookies(self): return self.cookies def get_field(self, name, default=None): return self.form.get(name, default) def get_fields(self): return self.form def get_method(self): """Returns the HTTP method for this request """ return self.environ.get('REQUEST_METHOD', 'GET') def formiter(self): return self.form.iteritems() def get_scheme(self): return self.scheme # The following environment variables are useful for reconstructing # the original URL, all of which are specified by CGI 1.1: # # SERVER_NAME "www.example.com" # SCRIPT_NAME "/q" # PATH_INFO "/debug/dump_sessions" # QUERY_STRING "session_id=10.27.8.40...." def get_server(self): """get_server() -> string Return the server name with an optional port number, eg. "www.example.com" or "foo.bar.com:8000". """ http_host = self.environ.get("HTTP_HOST") if http_host: return http_host server_name = self.environ["SERVER_NAME"].strip() server_port = self.environ.get("SERVER_PORT") if (not server_port or (self.get_scheme() == "http" and server_port == "80") or (self.get_scheme() == "https" and server_port == "443")): return server_name else: return server_name + ":" + server_port def get_path(self, n=0): """get_path(n : int = 0) -> string Return the path of the current request, chopping off 'n' path components from the right. Eg. if the path is "/bar/baz/qux", n=0 would return "/bar/baz/qux" and n=2 would return "/bar". Note that the query string, if any, is not included. A path with a trailing slash should just be considered as having an empty last component. Eg. if the path is "/bar/baz/", then: get_path(0) == "/bar/baz/" get_path(1) == "/bar/baz" get_path(2) == "/bar" If 'n' is negative, then components from the left of the path are returned. Continuing the above example, get_path(-1) = "/bar" get_path(-2) = "/bar/baz" get_path(-3) = "/bar/baz/" Raises ValueError if absolute value of n is larger than the number of path components.""" path_info = self.environ.get('PATH_INFO', '') path = self.environ['SCRIPT_NAME'] + path_info if n == 0: return path else: path_comps = path.split('/') if abs(n) > len(path_comps)-1: raise ValueError, "n=%d too big for path '%s'" % (n, path) if n > 0: return '/'.join(path_comps[:-n]) elif n < 0: return '/'.join(path_comps[:-n+1]) else: assert 0, "Unexpected value for n (%s)" % n def get_query(self): """() -> string Return the query component of the URL. """ return self.environ.get('QUERY_STRING', '') def get_path_query(self): """() -> string Return the path and the query string (if any). """ path = self.get_path() query = self.get_query() if query: path += '?' + query return path def get_url(self, n=0): """get_url(n : int = 0) -> string Return the URL of the current request, chopping off 'n' path components from the right. Eg. if the URL is "http://foo.com/bar/baz/qux", n=2 would return "http://foo.com/bar". Does not include the query string (if any). """ return "%s://%s%s" % (self.get_scheme(), self.get_server(), urllib.quote(self.get_path(n))) def get_environ(self, key, default=None): """get_environ(key : string) -> string Fetch a CGI environment variable from the request environment. See http://hoohoo.ncsa.uiuc.edu/cgi/env.html for the variables specified by the CGI standard. """ return self.environ.get(key, default) def get_encoding(self, encodings): """get_encoding(encodings : [string]) -> string Parse the "Accept-encoding" header. 'encodings' is a list of encodings supported by the server sorted in order of preference. The return value is one of 'encodings' or None if the client does not accept any of the encodings. """ accept_encoding = self.get_header("accept-encoding") or "" found_encodings = self._parse_pref_header(accept_encoding) if found_encodings: for encoding in encodings: if encoding in found_encodings: return encoding return None def get_accepted_types(self): """get_accepted_types() : {string:float} Return a dictionary mapping MIME types the client will accept to the corresponding quality value (1.0 if no value was specified). """ accept_types = self.environ.get('HTTP_ACCEPT', "") return self._parse_pref_header(accept_types) def _parse_pref_header(self, S): """_parse_pref_header(S:string) : {string:float} Parse a list of HTTP preferences (content types, encodings) and return a dictionary mapping strings to the quality value. """ found = {} # remove all linear whitespace S = _http_lws_re.sub("", S) for coding in _http_list_re.split(S): m = _http_encoding_re.match(coding) if m: encoding = m.group(1).lower() q = m.group(3) or 1.0 try: q = float(q) except ValueError: continue if encoding == "*": continue # stupid, ignore it if q > 0: found[encoding] = q return found def dump(self): result=[] row='%-15s %s' result.append("Form:") L = self.form.items() ; L.sort() for k,v in L: result.append(row % (k,v)) result.append("") result.append("Cookies:") L = self.cookies.items() ; L.sort() for k,v in L: result.append(row % (k,v)) result.append("") result.append("Environment:") L = self.environ.items() ; L.sort() for k,v in L: result.append(row % (k,v)) return "\n".join(result) def guess_browser_version(self): """guess_browser_version() -> (name : string, version : string) Examine the User-agent request header to try to figure out what the current browser is. Returns either (name, version) where each element is a string, (None, None) if we couldn't parse the User-agent header at all, or (name, None) if we got the name but couldn't figure out the version. Handles Microsoft's little joke of pretending to be Mozilla, eg. if the "User-Agent" header is Mozilla/5.0 (compatible; MSIE 5.5) returns ("MSIE", "5.5"). Konqueror does the same thing, and it's handled the same way. """ ua = self.get_header('user-agent') if ua is None: return (None, None) # The syntax for "User-Agent" in RFC 2616 is fairly simple: # # User-Agent = "User-Agent" ":" 1*( product | comment ) # product = token ["/" product-version ] # product-version = token # comment = "(" *( ctext | comment ) ")" # ctext = # token = 1* # tspecials = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | # "\" | <"> | "/" | "[" | "]" | "?" | "=" | "{" | # "}" | SP | HT # # This function handles the most-commonly-used subset of this syntax, # namely # User-Agent = "User-Agent" ":" product 1*SP [comment] # ie. one product string followed by an optional comment; # anything after that first comment is ignored. This should be # enough to distinguish Mozilla/Netscape, MSIE, Opera, and # Konqueror. m = _http_product_re.match(ua) if not m: import sys sys.stderr.write("couldn't parse User-Agent header: %r\n" % ua) return (None, None) name, version = m.groups() ua = ua[m.end():].lstrip() if ua.startswith('('): # we need to handle nested comments since MSIE uses them depth = 1 chars = [] for c in ua[1:]: if c == '(': depth += 1 elif c == ')': depth -= 1 if depth == 0: break elif depth == 1: # nested comments are discarded chars.append(c) comment = ''.join(chars) else: comment = '' if comment: comment_chunks = _comment_delim_re.split(comment) else: comment_chunks = [] if ("compatible" in comment_chunks and len(comment_chunks) > 1 and comment_chunks[1]): # A-ha! Someone is kidding around, pretending to be what # they are not. Most likely MSIE masquerading as Mozilla, # but lots of other clients (eg. Konqueror) do the same. real_ua = comment_chunks[1] if "/" in real_ua: (name, version) = real_ua.split("/", 1) else: if real_ua.startswith("MSIE") and ' ' in real_ua: (name, version) = real_ua.split(" ", 1) else: name = real_ua version = None return (name, version) # Either nobody is pulling our leg, or we didn't find anything # that looks vaguely like a user agent in the comment. So use # what we found outside the comment, ie. what the spec says we # should use (sigh). return (name, version) # guess_browser_version () # See RFC 2109 for details. Note that this parser is more liberal. _COOKIE_RE = re.compile(r""" \s* (?P[^=;,\s]+) \s* ( = \s* ( (?P "(\\[\x00-\x7f] | [^"])*") | (?P [^";,\s]*) ) )? \s* [;,]? """, re.VERBOSE) def parse_cookies(text): result = {} for m in _COOKIE_RE.finditer(text): name = m.group('name') if name[0] == '$': # discard, we don't handle per cookie attributes (e.g. $Path) continue qvalue = m.group('qvalue') if qvalue: value = re.sub(r'\\(.)', r'\1', qvalue)[1:-1] else: value = m.group('value') or '' result[name] = value return result SAFE_CHARS = string.letters + string.digits + "-@&+=_., " _safe_trans = None def make_safe_filename(s): global _safe_trans if _safe_trans is None: _safe_trans = ["_"] * 256 for c in SAFE_CHARS: _safe_trans[ord(c)] = c _safe_trans = "".join(_safe_trans) return s.translate(_safe_trans) class Upload: r""" Represents a single uploaded file. Uploaded files live in the filesystem, *not* in memory. fp an open file containing the content of the upload. The file pointer points to the beginning of the file orig_filename the complete filename supplied by the user-agent in the request that uploaded this file. Depending on the browser, this might have the complete path of the original file on the client system, in the client system's syntax -- eg. "C:\foo\bar\upload_this" or "/foo/bar/upload_this" or "foo:bar:upload_this". base_filename the base component of orig_filename, shorn of MS-DOS, Mac OS, and Unix path components and with "unsafe" characters neutralized (see make_safe_filename()) content_type the content type provided by the user-agent in the request that uploaded this file. charset the charset provide by the user-agent """ def __init__(self, orig_filename, content_type=None, charset=None): if orig_filename: self.orig_filename = orig_filename bspos = orig_filename.rfind("\\") cpos = orig_filename.rfind(":") spos = orig_filename.rfind("/") if bspos != -1: # eg. "\foo\bar" or "D:\ding\dong" filename = orig_filename[bspos+1:] elif cpos != -1: # eg. "C:foo" or ":ding:dong:foo" filename = orig_filename[cpos+1:] elif spos != -1: # eg. "foo/bar/baz" or "/tmp/blah" filename = orig_filename[spos+1:] else: filename = orig_filename self.base_filename = make_safe_filename(filename) else: self.orig_filename = None self.base_filename = None self.content_type = content_type self.charset = charset self.fp = None def receive(self, lines): self.fp = tempfile.TemporaryFile("w+b") for line in lines: self.fp.write(line) self.fp.seek(0) def __str__(self): return str(self.orig_filename) def __repr__(self): return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self) def read(self, n): return self.fp.read(n) def readline(self): return self.fp.readline() def readlines(self): return self.fp.readlines() def __iter__(self): return iter(self.fp) def close(self): self.fp.close() def get_size(self): """Return the size of the file, in bytes. """ if self.fp is None: return 0 else: return os.fstat(self.fp.fileno()).st_size class LineInput: r""" A wrapper for an input stream that has the following properties: * lines are terminated by \r\n * lines shorter than 'maxlength' are always returned unbroken * lines longer than 'maxlength' are broken but the pair of characters \r\n are never split * no more than 'length' characters are read from the underlying stream * if the underlying stream does not produce at least 'length' characters then EOFError is raised """ def __init__(self, fp, length): self.fp = fp self.length = length self.buf = '' def readline(self, maxlength=4096): # fill buffer n = min(self.length, maxlength - len(self.buf)) if n > 0: self.length -= n assert self.length >= 0 chunk = self.fp.read(n) if len(chunk) != n: raise EOFError('unexpected end of input') self.buf += chunk # split into lines buf = self.buf i = buf.find('\r\n') if i >= 0: i += 2 self.buf = buf[i:] return buf[:i] elif buf.endswith('\r'): # avoid splitting CR LF pairs self.buf = '\r' return buf[:-1] else: self.buf = '' return buf class MIMEInput: """ Split a MIME input stream into parts. Note that this class does not handle headers, transfer encoding, etc. """ def __init__(self, fp, boundary, length): self.lineinput = LineInput(fp, length) self.pat = re.compile(r'--%s(--)?[ \t]*\r\n' % re.escape(boundary)) self.done = False def moreparts(self): """Return true if there are more parts to be read.""" return not self.done def readpart(self): """Generate all the lines up to a MIME boundary. Note that you must exhaust the generator before calling this function again.""" assert not self.done last_line = '' while 1: line = self.lineinput.readline() if not line: # Hit EOF -- nothing more to read. This should *not* happen # in a well-formed MIME message. raise EOFError('MIME boundary not found (end of input)') if last_line.endswith('\r\n') or last_line == '': m = self.pat.match(line) if m: # If we hit the boundary line, return now. Forget # the current line *and* the CRLF ending of the # previous line. if m.group(1): # hit final boundary self.done = True yield last_line[:-2] return if last_line: yield last_line last_line = line Quixote-2.7b2/quixote/http_response.py0000664000175000017500000004723311326376155016361 0ustar nasnas"""quixote.http_response Provides the HTTPResponse class. """ import time try: import zlib except ImportError: pass import struct from rfc822 import formatdate import quixote from quixote.html import stringify status_reasons = { 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Moved Temporarily', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Time-out', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Request Entity Too Large', 414: 'Request-URI Too Large', 415: 'Unsupported Media Type', 416: 'Requested range not satisfiable', 417: 'Expectation Failed', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Time-out', 505: 'HTTP Version not supported', 507: 'Insufficient Storage', } _GZIP_HEADER = ("\037\213" # magic "\010" # compression method "\000" # flags "\000\000\000\000" # time, who cares? "\002" "\377") _GZIP_EXCLUDE = set(["application/pdf", "application/zip", "audio/mpeg", "image/gif", "image/jpeg", "image/png", "video/mpeg", "video/quicktime", "video/x-msvideo", ]) def _LOWU32(i): return i & 0xFFFFFFFFL class HTTPResponse: """ An object representation of an HTTP response. The Response type encapsulates all possible responses to HTTP requests. Responses are normally created by the Quixote publisher or by the HTTPRequest class (every request must have a response, after all). Instance attributes: content_type : string the MIME content type of the response (does not include extra params like charset) charset : string | None the character encoding of the the response. If none, the 'charset' parameter of the Context-Type header will not be included. status_code : int HTTP response status code (integer between 100 and 599) reason_phrase : string the reason phrase that accompanies status_code (usually set automatically by the set_status() method) headers : { string : string } most of the headers included with the response; every header set by 'set_header()' goes here. Does not include "Status" or "Set-Cookie" headers (unless someone uses set_header() to set them, but that would be foolish). body : str | Stream the response body, None by default. Note that if the body is not a stream then it is already encoded using 'charset'. buffered : bool if false, response data will be flushed as soon as it is written (the default is true). This is most useful for responses that use the Stream() protocol. Note that whether the client actually receives the partial response data is highly dependent on the web server cookies : { name:string : { attrname : value } } collection of cookies to set in this response; it is expected that the user-agent will remember the cookies and send them on future requests. The cookie value is stored as the "value" attribute. The other attributes are as specified by RFC 2109. cache : int | None the number of seconds the response may be cached. The default is 0, meaning don't cache at all. This variable is used to set the HTTP expires and cache-control headers. If set to None then no headers will be added. javascript_code : { string : string } a collection of snippets of JavaScript code to be included in the response. The collection is built by calling add_javascript(), but actually including the code in the HTML document is somebody else's problem. """ DEFAULT_CONTENT_TYPE = 'text/html' DEFAULT_CHARSET = None # defaults to quixote.DEFAULT_CHARSET def __init__(self, status=200, body=None, content_type=None, charset=None): """ Creates a new HTTP response. """ self.content_type = content_type or self.DEFAULT_CONTENT_TYPE self.charset = (charset or self.DEFAULT_CHARSET or quixote.DEFAULT_CHARSET) self.set_status(status) self.headers = {} if body is not None: self.set_body(body) else: self.body = None self.cookies = {} self.cache = 0 self.buffered = True self.javascript_code = None def set_content_type(self, content_type, charset=None): """(content_type : string, charset : string = None) Set the content type of the response to the MIME type specified by 'content_type'. If 'charset' is not provided and the content_type is text/* then the charset attribute remains unchanged, otherwise the charset attribute is set to None and the charset parameter will not be included as part of the Content-Type header. """ content_type = content_type.lower() if charset is not None or not content_type.startswith('text/'): self.charset = charset self.content_type = content_type def set_charset(self, charset): if not charset: self.charset = None else: self.charset = str(charset).lower() def set_status(self, status, reason=None): """set_status(status : int, reason : string = None) Sets the HTTP status code of the response. 'status' must be an integer in the range 100 .. 599. 'reason' must be a string; if not supplied, the default reason phrase for 'status' will be used. If 'status' is a non-standard status code, the generic reason phrase for its group of status codes will be used; eg. if status == 493, the reason for status 400 will be used. """ if not isinstance(status, int): raise TypeError, "status must be an integer" if not (100 <= status <= 599): raise ValueError, "status must be between 100 and 599" self.status_code = status if reason is None: if status in status_reasons: reason = status_reasons[status] else: # Eg. for generic 4xx failures, use the reason # associated with status 400. reason = status_reasons[status - (status % 100)] else: reason = str(reason) self.reason_phrase = reason def set_header(self, name, value): """set_header(name : string, value : string) Sets an HTTP return header "name" with value "value", clearing the previous value set for the header, if one exists. """ self.headers[name.lower()] = value def get_header(self, name, default=None): """get_header(name : string, default=None) -> value : string Gets an HTTP return header "name". If none exists then 'default' is returned. """ return self.headers.get(name.lower(), default) def set_expires(self, seconds=0, minutes=0, hours=0, days=0): if seconds is None: self.cache = None # don't generate 'Expires' header else: self.cache = seconds + 60*(minutes + 60*(hours + 24*days)) def _encode_chunk(self, chunk): """(chunk : str | unicode) -> str """ if isinstance(chunk, unicode): if self.charset is None: # iso-8859-1 is the default for the HTTP protocol if charset # parameter of content-type header is not provided chunk = chunk.encode('iso-8859-1') else: chunk = chunk.encode(self.charset) else: # we assume that the str is in the correct encoding or does # not contain character data pass return chunk def _compress_body(self, body): """(body: str) -> str """ n = len(body) co = zlib.compressobj(6, zlib.DEFLATED, -zlib.MAX_WBITS, zlib.DEF_MEM_LEVEL, 0) crc = zlib.crc32(body) chunks = [_GZIP_HEADER, co.compress(body), co.flush(), struct.pack(" 1.0: self.set_header("Content-Encoding", "gzip") return compressed_body else: return body def set_body(self, body, compress=False): """(body : any, compress : bool = False) Sets the response body equal to the argument 'body'. If 'compress' is true then the body may be compressed using 'gzip'. """ if not isinstance(body, Stream): body = self._encode_chunk(stringify(body)) if compress and self.content_type not in _GZIP_EXCLUDE: body = self._compress_body(body) self.body = body def expire_cookie(self, name, **attrs): """ Cause an HTTP cookie to be removed from the browser The response will include an HTTP header that will remove the cookie corresponding to "name" on the client, if one exists. This is accomplished by sending a new cookie with an expiration date that has already passed. Note that some clients require a path to be specified - this path must exactly match the path given when creating the cookie. The path can be specified as a keyword argument. """ dict = {'max_age': 0, 'expires': 'Thu, 01-Jan-1970 00:00:00 GMT'} dict.update(attrs) self.set_cookie(name, "deleted", **dict) def set_cookie(self, name, value, **attrs): """Set an HTTP cookie on the browser. The response will include an HTTP header that sets a cookie on cookie-enabled browsers with a key "name" and value "value". Cookie attributes such as "expires" and "domains" may be supplied as keyword arguments; see RFC 2109 for a full list. (For the "secure" and httponly attributes, use any true value.) This overrides any previous value for this cookie. Any previously-set attributes for the cookie are preserved, unless they are explicitly overridden with keyword arguments to this call. """ cookies = self.cookies if name in cookies: cookie = cookies[name] else: cookie = cookies[name] = {} cookie.update(attrs) cookie['value'] = value def add_javascript(self, code_id, code): """Add javascript code to be included in the response. code_id is used to ensure that the same piece of code is not included twice. The caller must be careful to avoid unintentional code_id and javascript identifier collisions. Note that the response object only provides a mechanism for collecting code -- actually including it in the HTML document that is the response body is somebody else's problem. (For an example, see Form._render_javascript().) """ if self.javascript_code is None: self.javascript_code = {code_id: code} elif code_id not in self.javascript_code: self.javascript_code[code_id] = code def redirect(self, location, permanent=False): """Cause a redirection without raising an error""" if not isinstance(location, str): raise TypeError, "location must be a string (got %s)" % `location` # Ensure that location is a full URL if location.find('://') == -1: raise ValueError, "URL must include the server name" if permanent: status = 301 else: status = 302 self.set_status(status) self.headers['location'] = location self.set_content_type('text/plain') return "Your browser should have redirected you to %s" % location def get_status_code(self): return self.status_code def get_reason_phrase(self): return self.reason_phrase def get_content_type(self): return self.content_type def get_content_length(self): if self.body is None: return None elif isinstance(self.body, Stream): return self.body.length else: return len(self.body) def _gen_cookie_headers(self): """_gen_cookie_headers() -> [string] Build a list of "Set-Cookie" headers based on all cookies set with 'set_cookie()', and return that list. """ cookie_headers = [] for name, attrs in self.cookies.items(): value = str(attrs['value']) if '"' in value: value = value.replace('"', '\\"') chunks = ['%s="%s"' % (name, value)] for name, val in attrs.items(): name = name.lower() if val is None: continue if name in ('expires', 'domain', 'path', 'max_age', 'comment'): name = name.replace('_', '-') chunks.append('%s=%s' % (name, val)) elif name == 'secure' and val: chunks.append("secure") elif name == 'httponly' and val: chunks.append("httponly") cookie_headers.append(("Set-Cookie", '; '.join(chunks))) return cookie_headers def generate_headers(self): """generate_headers() -> [(name:string, value:string)] Generate a list of headers to be returned as part of the response. """ headers = [] for name, value in self.headers.items(): headers.append((name.title(), value)) # All the "Set-Cookie" headers. if self.cookies: headers.extend(self._gen_cookie_headers()) # Date header now = time.time() if "date" not in self.headers: headers.append(("Date", formatdate(now))) # Cache directives if self.cache is None: pass # don't mess with the expires or cache control header else: # We add both an Expires header and a Cache-Control header # with a max-age directive. The max-age directive takes # priority when both Expires and max-age are present (even # if Expires is more restrictive, RFC 2616 section 14.9.3). if self.cache > 0: expire_date = formatdate(now + self.cache) cache_control = "max-age=%d" % self.cache else: # This is the default case and makes sense for a # dynamically generated response that can change on each # request. # # Using the current date is not a good idea since clocks # might not be synchronized. Any invalid date is treated # as in the past but Microsoft recommends "-1" for # Internet Explorer so that's what we use. expire_date = "-1" # The Expires header is sufficient for HTTP 1.0 but # for HTTP 1.1 we must add a must-revalidate directive. # Clients and proxies are allowed to ignore Expires in # certain cases and use stale pages (RFC 2616 sections # 13.1.5 and 14.9.4). cache_control = "max-age=0, must-revalidate" if ("expires" not in self.headers and "cache-control" not in self.headers): # If either of these headers are set then don't add # any of them. We assume the programmer knows what he # is doing in that case. headers.append(("Expires", expire_date)) headers.append(("Cache-Control", cache_control)) # Content-type if "content-type" not in self.headers: if self.charset is not None: value = '%s; charset=%s' % (self.content_type, self.charset) else: value = '%s' % self.content_type headers.append(('Content-Type', value)) # Content-Length if "content-length" not in self.headers: length = self.get_content_length() if length is not None: headers.append(('Content-Length', str(length))) return headers def generate_body_chunks(self): """Return a sequence of body chunks, encoded using 'charset'. """ if self.body is None: pass elif isinstance(self.body, Stream): for chunk in self.body: yield self._encode_chunk(chunk) else: yield self.body # already encoded def write(self, output, include_status=True, include_body=True): """(output:file, include_status:bool=True, include_body:bool=True) Write the HTTP response headers and, by default, body to 'output'. This is not a complete HTTP response, as it doesn't start with a response status line as specified by RFC 2616. By default, it does start with a "Status" header as described by the CGI spec. It is expected that this response is parsed by the web server and turned into a complete HTTP response. If include_body is False, only the headers are written to 'output'. This is used to support HTTP HEAD requests. """ flush_output = not self.buffered and hasattr(output, 'flush') if include_status: # "Status" header must come first. output.write("Status: %03d %s\r\n" % (self.status_code, self.reason_phrase)) for name, value in self.generate_headers(): output.write("%s: %s\r\n" % (name, value)) output.write("\r\n") if flush_output: output.flush() if not include_body: return for chunk in self.generate_body_chunks(): output.write(chunk) if flush_output: output.flush() if flush_output: output.flush() class Stream: """ A wrapper around response data that can be streamed. The 'iterable' argument must support the iteration protocol. Items returned by 'next()' must be strings. Beware that exceptions raised while writing the stream will not be handled gracefully. Instance attributes: iterable : any an object that supports the iteration protocol. The items produced by the stream must be strings. length: int | None the number of bytes that will be produced by the stream, None if it is not known. Used to set the Content-Length header. """ def __init__(self, iterable, length=None): self.iterable = iterable self.length = length def __iter__(self): return iter(self.iterable) Quixote-2.7b2/quixote/logger.py0000664000175000017500000000745111114154100014714 0ustar nasnasimport sys import os import codecs import time import socket import quixote from quixote.sendmail import sendmail class DefaultLogger: """ This is the default logger object used by the Quixote publisher. It controls access log and error log behavior. You may provide your own object if you wish to have different behavior. Instance attributes: access_log : file | None file to which every access will be logged. If None then access is not logged. error_log : file file to which application errors (exceptions caught by Quixote, as well as anything printed to stderr by application code) will be logged. Set to sys.stderr by default. error_email : string | None if set then internal server errors will cause messages to be sent to this address """ DEFAULT_CHARSET = None # defaults to quixote.DEFAULT_CHARSET def __init__(self, access_log=None, error_log=None, error_email=None): if access_log: self.access_log = self._open_log(access_log) else: self.access_log = None if error_log is None: self.error_log = sys.stderr else: self.error_log = self._open_log(error_log) self.error_email = error_email sys.stdout = self.error_log # print is handy for debugging def _open_log(self, filename): charset = self.DEFAULT_CHARSET or quixote.DEFAULT_CHARSET if charset == 'iso-8859-1': return open(filename, 'ab', 1) else: return codecs.open(filename, 'ab', encoding=charset, buffering=1) def log(self, msg): """ Write an message to the error log with a time stamp. """ timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) self.error_log.write("[%s] %s%s" % (timestamp, msg, os.linesep)) def log_internal_error(self, error_summary, error_msg): """(error_summary: str, error_msg: str) error_summary is a single line summary of the internal error, suitable for an email subject. error_msg is a multi-line plaintext message describing the error in detail. """ self.log("exception caught") self.error_log.write(error_msg) if self.error_email: sendmail('Quixote Traceback (%s)' % error_summary, error_msg, [self.error_email], from_addr=(self.error_email, socket.gethostname())) def log_request(self, request, start_time): """Log a request in the access_log file. """ if self.access_log is None: return if request.session: user = request.session.user or "-" else: user = "-" now = time.time() seconds = now - start_time timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(now)) request_uri = request.get_path() query = request.get_query() if query: request_uri += "?" + query proto = request.get_environ('SERVER_PROTOCOL') self.access_log.write('%s %s %s %d "%s %s %s" %s %r %0.2fsec%s' % (request.get_environ('REMOTE_ADDR'), user, timestamp, os.getpid(), request.get_method(), request_uri, proto, request.response.status_code, request.get_environ('HTTP_USER_AGENT', ''), seconds, os.linesep, )) Quixote-2.7b2/quixote/publish.py0000664000175000017500000003153111307241201015102 0ustar nasnas"""Logic for publishing modules and objects on the Web. """ import sys, traceback, StringIO import time import urlparse import cgitb from quixote.errors import PublishError, format_publish_error from quixote import util from quixote.config import Config from quixote.http_response import HTTPResponse from quixote.http_request import HTTPRequest from quixote.logger import DefaultLogger # Error message to dispay when DISPLAY_EXCEPTIONS in config file is not # true. Note that SERVER_ADMIN must be fetched from the environment and # plugged in here -- we can't do it now because the environment isn't # really setup for us yet if running as a FastCGI script. INTERNAL_ERROR_MESSAGE = """\ Internal Server Error

Internal Server Error

An internal error occurred while handling your request.

The server administrator should have been notified of the problem. You may wish to contact the server administrator (%s) and inform them of the time the error occurred, and anything you might have done to trigger the error.

If you are the server administrator, more information may be available in either the server's error log or Quixote's error log.

""" class Publisher: """ The core of Quixote and of any Quixote application. This class is responsible for converting each HTTP request into a traversal of the application's directory tree and, ultimately, a call of a Python function/method/callable object. Each invocation of a driver script should have one Publisher instance that lives for as long as the driver script itself. Eg. if your driver script is plain CGI, each Publisher instance will handle exactly one HTTP request; if you have a FastCGI driver, then each Publisher will handle every HTTP request handed to that driver script process. Instance attributes: root_directory : Directory the root directory that will be searched for objects to fulfill each request. This can be any object with a _q_traverse method that acts like Directory._q_traverse. logger : DefaultLogger controls access log and error log behavior session_manager : NullSessionManager keeps track of sessions config : Config holds all configuration info for this application. If the application doesn't provide values then default values from the quixote.config module are used. _request : HTTPRequest the HTTP request currently being processed. """ def __init__(self, root_directory, logger=None, session_manager=None, config=None, **kwargs): global _publisher if config is None: self.config = Config(**kwargs) else: if kwargs: raise ValueError("cannot provide both 'config' object and" " config arguments") self.config = config if logger is None: self.logger = DefaultLogger(error_log=self.config.error_log, access_log=self.config.access_log, error_email=self.config.error_email) else: self.logger = logger if session_manager is not None: self.session_manager = session_manager else: from quixote.session import NullSessionManager self.session_manager = NullSessionManager() if _publisher is not None: raise RuntimeError, "only one instance of Publisher allowed" _publisher = self if not hasattr(getattr(root_directory, '_q_traverse'), '__call__'): raise TypeError( 'Expected something with a _q_traverse method, got %r' % root_directory) self.root_directory = root_directory self._request = None def set_session_manager(self, session_manager): self.session_manager = session_manager def log(self, msg): self.logger.log(msg) def parse_request(self, request): """Parse the request information waiting in 'request'. """ request.process_inputs() def start_request(self): """Called at the start of each request. """ self.session_manager.start_request() def _set_request(self, request): """Set the current request object. """ self._request = request def _clear_request(self): """Unset the current request object. """ self._request = None def get_request(self): """Return the current request object. """ return self._request def finish_successful_request(self): """Called at the end of a successful request. """ self.session_manager.finish_successful_request() def format_publish_error(self, exc): return format_publish_error(exc) def finish_interrupted_request(self, exc): """ Called at the end of an interrupted request. Requests are interrupted by raising a PublishError exception. This method should return a string object which will be used as the result of the request. """ if not self.config.display_exceptions and exc.private_msg: exc.private_msg = None # hide it request = get_request() request.response = HTTPResponse(status=exc.status_code) output = self.format_publish_error(exc) self.session_manager.finish_successful_request() return output def finish_failed_request(self): """ Called at the end of an failed request. Any exception (other than PublishError) causes a request to fail. This method should return a string object which will be used as the result of the request. """ # build new response to be safe request = get_request() original_response = request.response request.response = HTTPResponse() #self.log("caught an error (%s), reporting it." % # sys.exc_info()[1]) (exc_type, exc_value, tb) = sys.exc_info() error_summary = traceback.format_exception_only(exc_type, exc_value) error_summary = error_summary[0][0:-1] # de-listify and strip newline plain_error_msg = self._generate_plaintext_error(request, original_response, exc_type, exc_value, tb) if not self.config.display_exceptions: # DISPLAY_EXCEPTIONS is false, so return the most # secure (and cryptic) page. request.response.set_header("Content-Type", "text/html") user_error_msg = self._generate_internal_error(request) elif self.config.display_exceptions == 'html': # Generate a spiffy HTML display using cgitb request.response.set_header("Content-Type", "text/html") user_error_msg = self._generate_cgitb_error(request, original_response, exc_type, exc_value, tb) else: # Generate a plaintext page containing the traceback request.response.set_header("Content-Type", "text/plain") user_error_msg = plain_error_msg self.logger.log_internal_error(error_summary, plain_error_msg) if exc_type is SystemExit: raise request.response.set_status(500) self.session_manager.finish_failed_request() return user_error_msg def _generate_internal_error(self, request): admin = request.get_environ('SERVER_ADMIN', "email address unknown") return INTERNAL_ERROR_MESSAGE % admin def _generate_plaintext_error(self, request, original_response, exc_type, exc_value, tb): error_file = StringIO.StringIO() # format the traceback traceback.print_exception(exc_type, exc_value, tb, file=error_file) # include request and response dumps error_file.write('\n') error_file.write(request.dump()) error_file.write('\n') return error_file.getvalue() def _generate_cgitb_error(self, request, original_response, exc_type, exc_value, tb): error_file = StringIO.StringIO() hook = cgitb.Hook(file=error_file) hook(exc_type, exc_value, tb) error_file.write('

Original Request

') error_file.write(str(util.dump_request(request))) error_file.write('

Original Response

')
        original_response.write(error_file)
        error_file.write('
') return error_file.getvalue() def try_publish(self, request): """(request : HTTPRequest) -> object The master method that does all the work for a single request. Exceptions are handled by the caller. """ self.start_request() path = request.get_environ('PATH_INFO', '') if path and path[:1] != '/': return redirect( request.get_environ('SCRIPT_NAME', '') + '/' + path, permanent=True) components = path[1:].split('/') output = self.root_directory._q_traverse(components) # The callable ran OK, commit any changes to the session self.finish_successful_request() return output def filter_output(self, request, output): """Hook for post processing the output. Subclasses may wish to override (e.g. check HTML syntax). """ return output def process_request(self, request): """(request : HTTPRequest) -> HTTPResponse Process a single request, given an HTTPRequest object. The try_publish() method will be called to do the work and exceptions will be handled here. """ self._set_request(request) start_time = time.time() try: self.parse_request(request) output = self.try_publish(request) except PublishError, exc: # Exit the publishing loop and return a result right away. output = self.finish_interrupted_request(exc) except: # Some other exception, generate error messages to the logs, etc. output = self.finish_failed_request() output = self.filter_output(request, output) self.logger.log_request(request, start_time) if output: if self.config.compress_pages and request.get_encoding(["gzip"]): compress = True else: compress = False request.response.set_body(output, compress) self._clear_request() return request.response def process(self, stdin, env): """(stdin : stream, env : dict) -> HTTPResponse Process a single request, given a stream, stdin, containing the incoming request and a dictionary, env, containing the web server's environment. An HTTPRequest object is created and the process_request() method is called and passed the request object. """ request = HTTPRequest(stdin, env) return self.process_request(request) # Publisher singleton, only one of these per process. _publisher = None def get_publisher(): return _publisher def get_request(): return _publisher.get_request() def get_response(): return _publisher.get_request().response def get_field(name, default=None): return _publisher.get_request().get_field(name, default) def get_cookie(name, default=None): return _publisher.get_request().get_cookie(name, default) def get_path(n=0): return _publisher.get_request().get_path(n) def redirect(location, permanent=False): """(location : string, permanent : boolean = false) -> string Create a redirection response. If the location is relative, then it will automatically be made absolute. The return value is an HTML document indicating the new URL (useful if the client browser does not honor the redirect). """ request = _publisher.get_request() location = urlparse.urljoin(request.get_url(), str(location)) return request.response.redirect(location, permanent) def get_session(): return _publisher.get_request().session def get_session_manager(): return _publisher.session_manager def get_user(): session = _publisher.get_request().session if session is None: return None else: return session.user def get_wsgi_app(): from quixote.wsgi import QWIP return QWIP(_publisher) def cleanup(): global _publisher _publisher = None Quixote-2.7b2/quixote/publish1.py0000664000175000017500000002431611307241201015166 0ustar nasnas"""Provides a publisher object that behaves like the Quixote 1 Publisher. Specifically, arbitrary namespaces may be exported and the HTTPRequest object is passed as the first argument to exported functions. Also, the _q_lookup(), _q_resolve(), and _q_access() methods work as they did in Quixote 1. """ import sys import re import types import warnings from quixote import errors, get_request, redirect from quixote.publish import Publisher as _Publisher from quixote.directory import Directory from quixote.html import htmltext class Publisher(_Publisher): """ Instance attributes: namespace_stack : [ module | instance | class ] """ def __init__(self, root_namespace, config=None): from quixote.config import Config if type(root_namespace) is types.StringType: root_namespace = _get_module(root_namespace) self.namespace_stack = [root_namespace] if config is None: config = Config() directory = RootDirectory(root_namespace, self.namespace_stack) _Publisher.__init__(self, directory, config=config) def debug(self, msg): self.log(msg) def get_namespace_stack(self): """get_namespace_stack() -> [ module | instance | class ] """ return self.namespace_stack class RootDirectory(Directory): def __init__(self, root_namespace, namespace_stack): self.root_namespace = root_namespace self.namespace_stack = namespace_stack def _q_traverse(self, path): # Initialize the publisher's namespace_stack del self.namespace_stack[:] request = get_request() # Traverse package to a (hopefully-) callable object object = _traverse_url(self.root_namespace, path, request, self.namespace_stack) # None means no output -- traverse_url() just issued a redirect. if object is None: return None # Anything else must be either a string... if isstring(object): output = object # ...or a callable. elif hasattr(object, '__call__'): output = object(request) if output is None: raise RuntimeError, 'callable %s returned None' % repr(object) # Uh-oh: 'object' is neither a string nor a callable. else: raise RuntimeError( "object is neither callable nor a string: %s" % repr(object)) return output def _get_module(name): """Get a module object by name.""" __import__(name) module = sys.modules[name] return module _slash_pat = re.compile("//*") def _traverse_url(root_namespace, path_components, request, namespace_stack): """(root_namespace : any, path_components : [string], request : HTTPRequest, namespace_stack : list) -> (object : any) Perform traversal based on the provided path, starting at the root object. It returns the script name and path info values for the arrived-at object, along with the object itself and a list of the namespaces traversed to get there. It's expected that the final object is something callable like a function or a method; intermediate objects along the way will usually be packages or modules. To prevent crackers from writing URLs that traverse private objects, every package, module, or object along the way must have a _q_exports attribute containing a list of publicly visible names. Not having a _q_exports attribute is an error, though having _q_exports be an empty list is OK. If a component of the path isn't in _q_exports, that also produces an error. Modifies the namespace_stack as it traverses the url, so that any exceptions encountered along the way can be handled by the nearest handler. """ path = '/' + '/'.join(path_components) # If someone accesses a Quixote driver script without a trailing # slash, we'll wind up here with an empty path. This won't # work; relative references in the page generated by the root # namespace's _q_index() will be off. Fix it by redirecting the # user to the right URL; when the client follows the redirect, # we'll wind up here again with path == '/'. if not path: return redirect(request.environ['SCRIPT_NAME'] + '/' , permanent=1) # Traverse starting at the root object = root_namespace namespace_stack.append(object) # Loop over the components of the path for component in path_components: if component == "": # "/q/foo/" == "/q/foo/_q_index" component = "_q_index" object = _get_component(object, component, request, namespace_stack) if not (isstring(object) or hasattr(object, '__call__')): # We went through all the components of the path and ended up at # something which isn't callable, like a module or an instance # without a __call__ method. if path[-1] != '/': if not request.form: # This is for the convenience of users who type in paths. # Repair the path and redirect. This should not happen for # URLs within the site. return redirect(request.get_path() + "/", permanent=1) else: # Automatic redirects disabled or there is form data. If # there is form data then the programmer is using the # wrong path. A redirect won't work if the form data came # from a POST anyhow. raise errors.TraversalError( "object is neither callable nor string " "(missing trailing slash?)", private_msg=repr(object), path=path) else: raise errors.TraversalError( "object is neither callable nor string", private_msg=repr(object), path=path) return object def _get_component(container, component, request, namespace_stack): """Get one component of a path from a namespace. """ # First security check: if the container doesn't even have an # _q_exports list, fail now: all Quixote-traversable namespaces # (modules, packages, instances) must have an export list! if not hasattr(container, '_q_exports'): raise errors.TraversalError( private_msg="%r has no _q_exports list" % container) # Second security check: call _q_access function if it's present. if hasattr(container, '_q_access'): # will raise AccessError if access failed container._q_access(request) # Third security check: make sure the current name component # is in the export list or is '_q_index'. If neither # condition is true, check for a _q_lookup() and call it. # '_q_lookup()' translates an arbitrary string into an object # that we continue traversing. (This is very handy; it lets # you put user-space objects into your URL-space, eliminating # the need for digging ID strings out of a query, or checking # PATHINFO after Quixote's done with it. But it is a # compromise to security: it opens up the traversal algorithm # to arbitrary names not listed in _q_exports!) If # _q_lookup() doesn't exist or is None, a TraversalError is # raised. # Check if component is in _q_exports. The elements in # _q_exports can be strings or 2-tuples mapping external names # to internal names. if component in container._q_exports or component == '_q_index': internal_name = component else: # check for an explicit external to internal mapping for value in container._q_exports: if type(value) is types.TupleType: if value[0] == component: internal_name = value[1] break else: internal_name = None if internal_name is None: # Component is not in exports list. object = None if hasattr(container, "_q_lookup"): object = container._q_lookup(request, component) elif hasattr(container, "_q_getname"): warnings.warn("_q_getname() on %s used; should " "be replaced by _q_lookup()" % type(container)) object = container._q_getname(request, component) if object is None: raise errors.TraversalError( private_msg="object %r has no attribute %r" % ( container, component)) # From here on, you can assume that the internal_name is not None elif hasattr(container, internal_name): # attribute is in _q_exports and exists object = getattr(container, internal_name) elif internal_name == '_q_index': if hasattr(container, "_q_lookup"): object = container._q_lookup(request, "") else: raise errors.AccessError( private_msg=("_q_index not found in %r" % container)) elif hasattr(container, "_q_resolve"): object = container._q_resolve(internal_name) if object is None: raise RuntimeError, ("component listed in _q_exports, " "but not returned by _q_resolve(%r)" % internal_name) else: # Set the object, so _q_resolve won't need to be called again. setattr(container, internal_name, object) elif type(container) is types.ModuleType: # try importing it as a sub-module. If we get an ImportError # here we don't catch it. It means that something that # doesn't exist was exported or an exception was raised from # deeper in the code. mod_name = container.__name__ + '.' + internal_name object = _get_module(mod_name) else: # a non-existent attribute is in _q_exports, # and the container is not a module. Give up. raise errors.TraversalError( private_msg=("%r in _q_exports list, " "but not found in %r" % (component, container))) namespace_stack.append(object) return object def isstring(x): return isinstance(x, (str, unicode, htmltext)) Quixote-2.7b2/quixote/sendmail.py0000664000175000017500000002264111307241147015243 0ustar nasnas"""quixote.sendmail Tools for sending mail from Quixote applications. """ import re from types import ListType, TupleType from smtplib import SMTP import quixote rfc822_specials_re = re.compile(r'[\(\)\<\>\@\,\;\:\\\"\.\[\]]') class RFC822Mailbox: """ In RFC 822, a "mailbox" is either a bare e-mail address or a bare e-mail address coupled with a chunk of text, most often someone's name. Eg. the following are all "mailboxes" in the RFC 822 grammar: luser@example.com Joe Luser Paddy O'Reilly "Smith, John" Dick & Jane "Tom, Dick, & Harry" This class represents an (addr_spec, real_name) pair and takes care of quoting the real_name according to RFC 822's rules for you. Just use the format() method and it will spit out a properly- quoted RFC 822 "mailbox". """ def __init__(self, *args): """RFC822Mailbox(addr_spec : string, name : string) RFC822Mailbox(addr_spec : string) RFC822Mailbox((addr_spec : string, name : string)) RFC822Mailbox((addr_spec : string)) Create a new RFC822Mailbox instance. The variety of call signatures is purely for your convenience. """ if (len(args) == 1 and type(args[0]) is TupleType): args = args[0] if len(args) == 1: addr_spec = args[0] real_name = None elif len(args) == 2: (addr_spec, real_name) = args else: raise TypeError( "invalid number of arguments: " "expected 1 or 2 strings or " "a tuple of 1 or 2 strings") self.addr_spec = addr_spec self.real_name = real_name def __str__(self): return self.addr_spec def __repr__(self): return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self) def format(self): if self.real_name and rfc822_specials_re.search(self.real_name): return '"%s" <%s>' % (self.real_name.replace('"', '\\"'), self.addr_spec) elif self.real_name: return '%s <%s>' % (self.real_name, self.addr_spec) else: return self.addr_spec def _ensure_mailbox(s): """_ensure_mailbox(s : string | (string,) | (string, string) | RFC822Mailbox | None) -> RFC822Mailbox | None If s is a string, or a tuple of 1 or 2 strings, returns an RFC822Mailbox encapsulating them as an addr_spec and real_name. If s is already an RFC822Mailbox, returns s. If s is None, returns None. """ if s is None or isinstance(s, RFC822Mailbox): return s else: return RFC822Mailbox(s) # Maximum number of recipients that will be explicitly listed in # any single message header. Eg. if MAX_HEADER_RECIPIENTS is 10, # there could be up to 10 "To" recipients and 10 "CC" recipients # explicitly listed in the message headers. MAX_HEADER_RECIPIENTS = 10 def _add_recip_headers(headers, field_name, addrs): if not addrs: return addrs = [addr.format() for addr in addrs] if len(addrs) == 1: headers.append("%s: %s" % (field_name, addrs[0])) elif len(addrs) <= MAX_HEADER_RECIPIENTS: headers.append("%s: %s," % (field_name, addrs[0])) for addr in addrs[1:-1]: headers.append(" %s," % addr) headers.append(" %s" % addrs[-1]) else: headers.append("%s: (long recipient list suppressed) : ;" % field_name) def sendmail(subject, msg_body, to_addrs, from_addr=None, cc_addrs=None, extra_headers=None, smtp_sender=None, smtp_recipients=None, mail_server=None, mail_debug_addr=None, config=None): """ Send an email message to a list of recipients via a local SMTP server. In normal use, you supply a list of primary recipient e-mail addresses in 'to_addrs', an optional list of secondary recipient addresses in 'cc_addrs', and a sender address in 'from_addr'. sendmail() then constructs a message using those addresses, 'subject', and 'msg_body', and mails the message to every recipient address. (Specifically, it connects to the mail server named in the MAIL_SERVER config variable -- default "localhost" -- and instructs the server to send the message to every recipient address in 'to_addrs' and 'cc_addrs'.) 'from_addr' is optional because web applications often have a common e-mail sender address, such as "webmaster@example.com". Just set the Quixote config variable MAIL_FROM, and it will be used as the default sender (both header and envelope) for all e-mail sent by sendmail(). E-mail addresses can be specified a number of ways. The most efficient is to supply instances of RFC822Mailbox, which bundles a bare e-mail address (aka "addr_spec" from the RFC 822 grammar) and real name together in a readily-formattable object. You can also supply an (addr_spec, real_name) tuple, or an addr_spec on its own. The latter two are converted into RFC822Mailbox objects for formatting, which is why it may be more efficient to construct RFC822Mailbox objects yourself. Thus, the following are all equivalent in terms of who gets the message: sendmail(to_addrs=["joe@example.com"], ...) sendmail(to_addrs=[("joe@example.com", "Joe User")], ...) sendmail(to_addrs=[RFC822Mailbox("joe@example.com", "Joe User")], ...) ...although the "To" header will be slightly different. In the first case, it will be To: joe@example.com while in the other two, it will be: To: Joe User which is a little more user-friendly. In more advanced usage, you might wish to specify the SMTP sender and recipient addresses separately. For example, if you want your application to send mail to users that looks like it comes from a real human being, but you don't want that human being to get the bounce messages from the mailing, you might do this: sendmail(to_addrs=user_list, ..., from_addr=("realuser@example.com", "A Real User"), smtp_sender="postmaster@example.com") End users will see mail from "A Real User " in their inbox, but bounces will go to postmaster@example.com. One use of different header and envelope recipients is for testing/debugging. If you want to test that your application is sending the right mail to bigboss@example.com without filling bigboss' inbox with dross, you might do this: sendmail(to_addrs=["bigboss@example.com"], ..., smtp_recipients=["developers@example.com"]) This is so useful that it's a Quixote configuration option: just set MAIL_DEBUG_ADDR to (eg.) "developers@example.com", and every message that sendmail() would send out is diverted to the debug address. Generally raises an exception on any SMTP errors; see smtplib (in the standard library documentation) for details. """ if not mail_server and config is None: from quixote import get_publisher config = get_publisher().config from_addr = from_addr or config.mail_from mail_server = mail_server or config.mail_server if config is not None: mail_debug_addr = mail_debug_addr or config.mail_debug_addr if not isinstance(to_addrs, ListType): raise TypeError("'to_addrs' must be a list") if not (cc_addrs is None or isinstance(cc_addrs, ListType)): raise TypeError("'cc_addrs' must be a list or None") # Make sure we have a "From" address if from_addr is None: raise RuntimeError( "no from_addr supplied, and MAIL_FROM not set in config file") # Ensure all of our addresses are really RFC822Mailbox objects. from_addr = _ensure_mailbox(from_addr) to_addrs = map(_ensure_mailbox, to_addrs) if cc_addrs: cc_addrs = map(_ensure_mailbox, cc_addrs) # Start building the message headers. headers = ["From: %s" % from_addr.format(), "Subject: %s" % subject] _add_recip_headers(headers, "To", to_addrs) if cc_addrs: _add_recip_headers(headers, "Cc", cc_addrs) if extra_headers: headers.extend(extra_headers) if mail_debug_addr: debug1 = ("[debug mode, message actually sent to %s]\n" % mail_debug_addr) if smtp_recipients: debug2 = ("[original SMTP recipients: %s]\n" % ", ".join(smtp_recipients)) else: debug2 = "" sep = ("-"*72) + "\n" msg_body = debug1 + debug2 + sep + msg_body smtp_recipients = [mail_debug_addr] if smtp_sender is None: smtp_sender = from_addr.addr_spec else: smtp_sender = _ensure_mailbox(smtp_sender).addr_spec if smtp_recipients is None: smtp_recipients = [addr.addr_spec for addr in to_addrs] if cc_addrs: smtp_recipients.extend([addr.addr_spec for addr in cc_addrs]) else: smtp_recipients = [_ensure_mailbox(recip).addr_spec for recip in smtp_recipients] message = "\n".join(headers) + "\n\n" + msg_body smtp = SMTP(mail_server) smtp.sendmail(smtp_sender, smtp_recipients, message) smtp.quit() Quixote-2.7b2/quixote/session.py0000664000175000017500000005072411307241555015140 0ustar nasnas"""Quixote session management. There are two levels to Quixote's session management system: - SessionManager - Session A SessionManager is responsible for creating sessions, setting and reading session cookies, maintaining the collection of all sessions, and so forth. There is one SessionManager instance per Quixote process. A Session is the umbrella object for a single session (notionally, a (user, host, browser_process) triple). Simple applications can probably get away with putting all session data into a Session object (or, better, into an application-specific subclass of Session). The default implementation provided here is not persistent: when the Quixote process shuts down, all session data is lost. See doc/session-mgmt.txt for information on session persistence. """ from time import time, localtime, strftime from quixote import get_publisher, get_cookie, get_response, get_request, \ get_session from quixote.util import randbytes class NullSessionManager: """A session manager that does nothing. It is the default session manager. """ def start_request(self): """ Called near the beginning of each request: after the HTTPRequest object has been built, but before we traverse the URL or call the callable object found by URL traversal. """ def finish_successful_request(self): """Called near the end of each successful request. Not called if there were any errors processing the request. """ def finish_failed_request(self): """Called near the end of a failed request (i.e. a exception that was not a PublisherError was raised. """ class SessionManager: """ SessionManager acts as a dictionary of all sessions, mapping session ID strings to individual session objects. Session objects are instances of Session (or a custom subclass for your application). SessionManager is also responsible for creating and destroying sessions, for generating and interpreting session cookies, and for session persistence (if any -- this implementation is not persistent). Most applications can just use this class directly; sessions will be kept in memory-based dictionaries, and will be lost when the Quixote process dies. Alternatively an application can subclass SessionManager to implement specific behaviour, such as persistence. Instance attributes: session_class : class the class that is instantiated to create new session objects (in new_session()) sessions : mapping { session_id:string : Session } the collection of sessions managed by this SessionManager """ ACCESS_TIME_RESOLUTION = 1 # in seconds def __init__(self, session_class=None, session_mapping=None): """(session_class : class = Session, session_mapping : mapping = None) Create a new session manager. There should be one session manager per publisher, ie. one per process session_class is used by the new_session() method -- it returns an instance of session_class. """ self.sessions = {} if session_class is None: self.session_class = Session else: self.session_class = session_class if session_mapping is None: self.sessions = {} else: self.sessions = session_mapping def __repr__(self): return "<%s at %x>" % (self.__class__.__name__, id(self)) # -- Mapping interface --------------------------------------------- # (subclasses shouldn't need to override any of this, unless # your application passes in a session_mapping object that # doesn't provide all of the mapping methods needed here) def keys(self): """() -> [string] Return the list of session IDs of sessions in this session manager. """ return self.sessions.keys() def sorted_keys(self): """() -> [string] Return the same list as keys(), but sorted. """ keys = self.keys() keys.sort() return keys def values(self): """() -> [Session] Return the list of sessions in this session manager. """ return self.sessions.values() def items(self): """() -> [(string, Session)] Return the list of (session_id, session) pairs in this session manager. """ return self.sessions.items() def get(self, session_id, default=None): """(session_id : string, default : any = None) -> Session Return the session object identified by 'session_id', or None if no such session. """ return self.sessions.get(session_id, default) def __iter__(self): return self.sessions.itervalues() def __getitem__(self, session_id): """(session_id : string) -> Session Return the session object identified by 'session_id'. Raise KeyError if no such session. """ return self.sessions[session_id] def has_key(self, session_id): """(session_id : string) -> boolean Return true if a session identified by 'session_id' exists in the session manager. """ return session_id in self.sessions def __contains__(self, session_id): return self.has_key(session_id) def has_session(self, session_id): return self.has_key(session_id) def __setitem__(self, session_id, session): """(session_id : string, session : Session) Store 'session' in the session manager under 'session_id'. """ if not isinstance(session, self.session_class): raise TypeError("session not an instance of %r: %r" % (self.session_class, session)) assert session.id is not None, "session ID not set" assert session_id == session.id, "session ID mismatch" self.sessions[session_id] = session def __delitem__(self, session_id): """(session_id : string) -> Session Remove the session object identified by 'session_id' from the session manager. Raise KeyError if no such session. """ del self.sessions[session_id] # -- Transactional interface --------------------------------------- # Useful for applications that provide a transaction-oriented # persistence mechanism. You'll still need to provide a mapping # object that works with your persistence mechanism; these two # methods let you hook into your transaction machinery after a # request is finished processing. def abort_changes(self, session): """(session : Session) Placeholder for subclasses that implement transactional persistence: forget about saving changes to the current session. Called by the publisher when a request fails, ie. when it catches an exception other than PublishError. """ pass def commit_changes(self, session): """(session : Session) Placeholder for subclasses that implement transactional persistence: commit changes to the current session. Called by the publisher when a request completes successfully, or is interrupted by a PublishError exception. """ pass # -- Session management -------------------------------------------- # these build on the storage mechanism implemented by the # above mapping methods, and are concerned with all the high- # level details of managing web sessions def new_session(self, id): """(id : string) -> Session Return a new session object, ie. an instance of the session_class class passed to the constructor (defaults to Session). """ return self.session_class(id) def _get_session_id(self, config): """() -> string Find the ID of the current session by looking for the session cookie in the request. Return None if no such cookie or the cookie has been expired, otherwise return the cookie's value. """ id = get_cookie(config.session_cookie_name) if id == "" or id == "*del*": return None else: return id def _make_session_id(self): # Generate a session ID, which is just the value of the session # cookie we are about to drop on the user. (It's also the key # used with the session manager mapping interface.) id = None while id is None or self.has_session(id): id = randbytes(8) # 64-bit random number return id def _create_session(self): # Create a new session object, with no ID for now - one will # be assigned later if we save the session. return self.new_session(None) def get_session(self): """() -> Session Fetch or create a session object for the current session, and return it. If a session cookie is found in the HTTP request object, use it to look up and return an existing session object. If no session cookie is found, create a new session. Note that this method does *not* cause the new session to be stored in the session manager, nor does it drop a session cookie on the user. Those are both the responsibility of maintain_session(), called at the end of a request. """ config = get_publisher().config id = self._get_session_id(config) session = self.get(id) or self._create_session() session._set_access_time(self.ACCESS_TIME_RESOLUTION) return session def maintain_session(self, session): """(session : Session) Maintain session information. This method is called after servicing an HTTP request, just before the response is returned. If a session contains information it is saved and a cookie dropped on the client. If not, the session is discarded and the client will be instructed to delete the session cookie (if any). """ if not session.has_info(): # Session has no useful info -- forget it. If it previously # had useful information and no longer does, we have to # explicitly forget it. if session.id and self.has_session(session.id): del self[session.id] self.revoke_session_cookie() return if session.id is None: # This is the first time this session has had useful # info -- store it and set the session cookie. session.id = self._make_session_id() self[session.id] = session self.set_session_cookie(session.id) elif session.is_dirty(): # We have already stored this session, but it's dirty # and needs to be stored again. This will never happen # with the default Session class, but it's there for # applications using a persistence mechanism that requires # repeatedly storing the same object in the same mapping. self[session.id] = session def _set_cookie(self, value, **attrs): config = get_publisher().config name = config.session_cookie_name if config.session_cookie_path: path = config.session_cookie_path else: path = get_request().get_environ('SCRIPT_NAME') if not path.endswith("/"): path += "/" domain = config.session_cookie_domain attrs = attrs.copy() if config.session_cookie_secure: attrs['secure'] = 1 if config.session_cookie_httponly: attrs['httponly'] = 1 get_response().set_cookie(name, value, domain=domain, path=path, **attrs) return name def set_session_cookie(self, session_id): """(session_id : string) Ensure that a session cookie with value 'session_id' will be returned to the client via the response object. """ self._set_cookie(session_id) def revoke_session_cookie(self): """ Remove the session cookie from the remote user's session by resetting the value and maximum age in the response object. Also remove the cookie from the request so that further processing of this request does not see the cookie's revoked value. """ cookie_name = self._set_cookie("", max_age=0) if get_cookie(cookie_name) is not None: del get_request().cookies[cookie_name] def expire_session(self): """ Expire the current session, ie. revoke the session cookie from the client and remove the session object from the session manager and from the current request. """ self.revoke_session_cookie() request = get_request() try: del self[request.session.id] except KeyError: # This can happen if the current session hasn't been saved # yet, eg. if someone tries to leave a session with no # interesting data. That's not a big deal, so ignore it. pass request.session = None def has_session_cookie(self, must_exist=False): """(must_exist : boolean = false) -> bool Return true if the request already has a cookie identifying a session object. If 'must_exist' is true, the cookie must correspond to a currently existing session; otherwise (the default), we just check for the existence of the session cookie and don't inspect its content at all. """ config = get_publisher().config id = get_cookie(config.session_cookie_name) if id is None: return False if must_exist: return self.has_session(id) else: return True # -- Hooks into the Quixote main loop ------------------------------ def start_request(self): """ Called near the beginning of each request: after the HTTPRequest object has been built, but before we traverse the URL or call the callable object found by URL traversal. """ session = self.get_session() get_request().session = session session.start_request() def finish_successful_request(self): """Called near the end of each successful request. Not called if there were any errors processing the request. """ session = get_session() if session is not None: self.maintain_session(session) self.commit_changes(session) def finish_failed_request(self): """Called near the end of a failed request (i.e. a exception that was not a PublisherError was raised. """ self.abort_changes(get_session()) class Session: """ Holds information about the current session. The only information that is likely to be useful to applications is the 'user' attribute, which applications can use as they please. Instance attributes: id : string the session ID (generated by SessionManager and used as the value of the session cookie) user : any an object to identify the human being on the other end of the line. It's up to you whether to store just a string in 'user', or some more complex data structure or object. _remote_address : string IP address of user owning this session (only set when the session is created) _creation_time : float _access_time : float two ways of keeping track of the "age" of the session. Note that '__access_time' is maintained by the SessionManager that owns this session, using _set_access_time(). _form_tokens : [string] outstanding form tokens. This is used as a queue that can grow up to MAX_FORM_TOKENS. Tokens are removed when forms are submitted. Feel free to access 'id' and 'user' directly, but do not modify 'id'. The preferred way to set 'user' is with the set_user() method (which you might want to override for type-checking). """ MAX_FORM_TOKENS = 16 # maximum number of outstanding form tokens def __init__(self, id): self.id = id self.user = None self._remote_address = get_request().get_environ("REMOTE_ADDR") self._creation_time = self._access_time = time() self._form_tokens = [] # queue def __repr__(self): return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self.id) def __str__(self): if self.user: return "session %s (user %s)" % (self.id, self.user) else: return "session %s (no user)" % self.id def has_info(self): """() -> boolean Return true if this session contains any information that must be saved. """ return self.user or self._form_tokens def is_dirty(self): """() -> boolean Return true if this session has changed since it was last saved such that it needs to be saved again. Default implementation always returns false since the default storage mechanism is an in-memory dictionary, and you don't have to put the same object into the same slot of a dictionary twice. If sessions are stored to, eg., files in a directory or slots in a hash file, is_dirty() should probably be an alias or wrapper for has_info(). See doc/session-mgmt.txt. """ return False def dump(self, file=None, header=True, deep=True): time_fmt = "%Y-%m-%d %H:%M:%S" ctime = strftime(time_fmt, localtime(self._creation_time)) atime = strftime(time_fmt, localtime(self._access_time)) if header: file.write('session %s:' % self.id) file.write(' user %s' % self.user) file.write(' _remote_address: %s' % self._remote_address) file.write(' created %s, last accessed %s' % (ctime, atime)) file.write(' _form_tokens: %s\n' % self._form_tokens) def start_request(self): """ Called near the beginning of each request: after the HTTPRequest object has been built, but before we traverse the URL or call the callable object found by URL traversal. """ if self.user is not None: get_request().environ['REMOTE_USER'] = str(self.user) # -- Simple accessors and modifiers -------------------------------- def set_user(self, user): self.user = user def get_user(self): return self.user def get_remote_address(self): """Return the IP address (dotted-quad string) that made the initial request in this session. """ return self._remote_address def get_creation_time(self): """Return the time that this session was created (seconds since epoch). """ return self._creation_time def get_access_time(self): """Return the time that this session was last accessed (seconds since epoch). """ return self._access_time def get_creation_age(self, _now=None): """Return the number of seconds since session was created.""" # _now arg is not strictly necessary, but there for consistency # with get_access_age() return (_now or time()) - self._creation_time def get_access_age(self, _now=None): """Return the number of seconds since session was last accessed.""" # _now arg is for SessionManager's use return (_now or time()) - self._access_time # -- Methods for SessionManager only ------------------------------- def _set_access_time(self, resolution): now = time() if now - self._access_time > resolution: self._access_time = now # -- Form token methods -------------------------------------------- def create_form_token(self): """() -> string Create a new form token and add it to a queue of outstanding form tokens for this session. A maximum of MAX_FORM_TOKENS are saved. The new token is returned. """ token = randbytes(8) self._form_tokens.append(token) extra = len(self._form_tokens) - self.MAX_FORM_TOKENS if extra > 0: del self._form_tokens[:extra] return token def has_form_token(self, token): """(token : string) -> boolean Return true if 'token' is in the queue of outstanding tokens. """ return token in self._form_tokens def remove_form_token(self, token): """(token : string) Remove 'token' from the queue of outstanding tokens. """ self._form_tokens.remove(token) Quixote-2.7b2/quixote/util.py0000664000175000017500000003230511307241201014411 0ustar nasnas"""quixote.util Contains various useful functions and classes: xmlrpc(request, func) : Processes the body of an XML-RPC request, and calls 'func' with the method name and parameters. StaticFile : Wraps a file from a filesystem as a Quixote resource. StaticDirectory : Wraps a directory containing static files as a Quixote directory. StaticFile and StaticDirectory were contributed by Hamish Lawson. See doc/static-files.txt for examples of their use. """ import sys import os import time import binascii import mimetypes import urllib import xmlrpclib from rfc822 import formatdate import quixote from quixote import errors from quixote.directory import Directory from quixote.html import htmltext, TemplateIO from quixote.http_response import Stream if hasattr(os, 'urandom'): # available in Python 2.4 and also works on win32 def randbytes(bytes): """Return bits of random data as a hex string.""" return binascii.hexlify(os.urandom(bytes)) elif os.path.exists('/dev/urandom'): # /dev/urandom is just as good as /dev/random for cookies (assuming # SHA-1 is secure) and it never blocks. def randbytes(bytes): """Return bits of random data as a hex string.""" return binascii.hexlify(open("/dev/urandom").read(bytes)) else: # this is much less secure than the above function import sha class _PRNG: def __init__(self): self.state = sha.new(str(time.time() + time.clock())) self.count = 0 def _get_bytes(self): self.state.update('%s %d' % (time.time() + time.clock(), self.count)) self.count += 1 return self.state.hexdigest() def randbytes(self, bytes): """Return bits of random data as a hex string.""" s = "" chars = 2*bytes while len(s) < chars: s += self._get_bytes() return s[:chars] randbytes = _PRNG().randbytes def import_object(name): i = name.rfind('.') if i != -1: module_name = name[:i] object_name = name[i+1:] __import__(module_name) return getattr(sys.modules[module_name], object_name) else: __import__(name) return sys.modules[name] def xmlrpc(request, func): """xmlrpc(request:Request, func:callable) : string Processes the body of an XML-RPC request, and calls 'func' with two arguments, a string containing the method name and a tuple of parameters. """ # Get contents of POST body if request.get_method() != 'POST': request.response.set_status(405, "Only the POST method is accepted") return "XML-RPC handlers only accept the POST method." length = int(request.environ['CONTENT_LENGTH']) data = request.stdin.read(length) # Parse arguments params, method = xmlrpclib.loads(data) try: result = func(method, params) except xmlrpclib.Fault, exc: result = exc except: # report exception back to client result = xmlrpclib.dumps( xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)) ) else: result = (result,) result = xmlrpclib.dumps(result, methodresponse=1) request.response.set_content_type('text/xml') return result class FileStream(Stream): CHUNK_SIZE = 20000 def __init__(self, fp, size=None): self.fp = fp self.length = size def __iter__(self): return self def next(self): chunk = self.fp.read(self.CHUNK_SIZE) if not chunk: raise StopIteration return chunk class StaticFile: """ Wrapper for a static file on the filesystem. """ def __init__(self, path, follow_symlinks=False, mime_type=None, encoding=None, cache_time=None): """StaticFile(path:string, follow_symlinks:bool) Initialize instance with the absolute path to the file. If 'follow_symlinks' is true, symbolic links will be followed. 'mime_type' specifies the MIME type, and 'encoding' the encoding; if omitted, the MIME type will be guessed, defaulting to text/plain. Optional cache_time parameter indicates the number of seconds a response is considered to be valid, and will be used to set the Expires header in the response when quixote gets to that part. If the value is None then the Expires header will not be set. """ # Check that the supplied path is absolute and (if a symbolic link) may # be followed self.path = path if not os.path.isabs(path): raise ValueError, "Path %r is not absolute" % path # Decide the Content-Type of the file guess_mime, guess_enc = mimetypes.guess_type(os.path.basename(path), strict=False) self.mime_type = mime_type or guess_mime or 'text/plain' self.encoding = encoding or guess_enc or None self.cache_time = cache_time self.follow_symlinks = follow_symlinks def __call__(self): if not self.follow_symlinks and os.path.islink(self.path): raise errors.TraversalError(private_msg="Path %r is a symlink" % self.path) request = quixote.get_request() response = quixote.get_response() if self.cache_time is None: response.set_expires(None) # don't set the Expires header else: # explicitly allow client to cache page by setting the Expires # header, this is even more efficient than the using # Last-Modified/If-Modified-Since since the browser does not need # to contact the server response.set_expires(seconds=self.cache_time) stat = os.stat(self.path) last_modified = formatdate(stat.st_mtime) if last_modified == request.get_header('If-Modified-Since'): # handle exact match of If-Modified-Since header response.set_status(304) return '' # Set the Content-Type for the response and return the file's contents. response.set_content_type(self.mime_type) if self.encoding: response.set_header("Content-Encoding", self.encoding) response.set_header('Last-Modified', last_modified) return FileStream(open(self.path, 'rb'), stat.st_size) class StaticDirectory(Directory): """ Wrap a filesystem directory containing static files as a Quixote directory. """ _q_exports = [''] FILE_CLASS = StaticFile def __init__(self, path, use_cache=False, list_directory=False, follow_symlinks=False, cache_time=None, file_class=None, index_filenames=None): """(path:string, use_cache:bool, list_directory:bool, follow_symlinks:bool, cache_time:int, file_class=None, index_filenames:[string]) Initialize instance with the absolute path to the file. If 'use_cache' is true, StaticFile instances will be cached in memory. If 'list_directory' is true, users can request a directory listing. If 'follow_symlinks' is true, symbolic links will be followed. Optional parameter cache_time allows setting of Expires header in response object (see note for StaticFile for more detail). Optional parameter 'index_filenames' specifies a list of filenames to be used as index files in the directory. First file found searching left to right is returned. """ # Check that the supplied path is absolute self.path = path if not os.path.isabs(path): raise ValueError, "Path %r is not absolute" % path self.use_cache = use_cache self.cache = {} self.list_directory = list_directory self.follow_symlinks = follow_symlinks self.cache_time = cache_time if file_class is not None: self.file_class = file_class else: self.file_class = self.FILE_CLASS self.index_filenames = index_filenames def _render_header(self, title): r = TemplateIO(html=True) r += htmltext('') r += htmltext('') r += htmltext('%s') % title r += htmltext('') r += htmltext("

%s

") % title return r.getvalue() def _render_footer(self): return htmltext('') def _q_index(self): """ If directory listings are allowed, generate a simple HTML listing of the directory's contents with each item hyperlinked; if the item is a subdirectory, place a '/' after it. If not allowed, return a page to that effect. """ if self.index_filenames: for name in self.index_filenames: try: obj = self._q_lookup(name) except errors.TraversalError: continue if (not isinstance(obj, StaticDirectory) and hasattr(obj, '__call__')): return obj() r = TemplateIO(html=True) if self.list_directory: r += self._render_header('Index of %s' % quixote.get_path()) template = htmltext('%s%s\n') r += htmltext('
')
            r += template % ('..', '..', '')
            files = os.listdir(self.path)
            files.sort()
            for filename in files:
                filepath = os.path.join(self.path, filename)
                marker = os.path.isdir(filepath) and "/" or ""
                r += template % (urllib.quote(filename), filename, marker)
            r += htmltext('
') r += self._render_footer() else: r += self._render_header('Directory listing denied') r += htmltext('

This directory does not allow its contents ' 'to be listed.

') r += self._render_footer() return r.getvalue() def _q_lookup(self, name): """ Get a file from the filesystem directory and return the StaticFile or StaticDirectory wrapper of it; use caching if that is in use. """ if name in ('.', '..'): raise errors.TraversalError(private_msg="Attempt to use '.', '..'") if name in self.cache: # Get item from cache item = self.cache[name] else: # Get item from filesystem; cache it if caching is in use. item_filepath = os.path.join(self.path, name) while os.path.islink(item_filepath): if not self.follow_symlinks: raise errors.TraversalError else: dest = os.readlink(item_filepath) item_filepath = os.path.join(self.path, dest) if os.path.isdir(item_filepath): item = self.__class__(item_filepath, self.use_cache, self.list_directory, self.follow_symlinks, self.cache_time, self.file_class, self.index_filenames) elif os.path.isfile(item_filepath): item = self.file_class(item_filepath, self.follow_symlinks, cache_time=self.cache_time) else: raise errors.TraversalError if self.use_cache: self.cache[name] = item return item class Redirector: """ A simple class that can be used from inside _q_lookup() to redirect requests. """ _q_exports = [] def __init__(self, location, permanent=False): self.location = location self.permanent = permanent def _q_lookup(self, component): return self def __call__(self): return quixote.redirect(self.location, self.permanent) def dump_request(request=None): if request is None: request = quixote.get_request() """Dump an HTTPRequest object as HTML.""" row_fmt = htmltext('%s%s') r = TemplateIO(html=True) r += htmltext('

form

' '') for k, v in request.form.items(): r += row_fmt % (k, v) r += htmltext('
' '

cookies

' '') for k, v in request.cookies.items(): r += row_fmt % (k, v) r += htmltext('
' '

environ

' '') for k, v in request.environ.items(): r += row_fmt % (k, v) r += htmltext('
') return r.getvalue() def get_directory_path(): """() -> [object] Return the list of traversed instances. """ path = [] frame = sys._getframe() while frame: if frame.f_code.co_name == '_q_traverse': self = frame.f_locals.get('self', None) if path[:1] != [self]: path.insert(0, self) frame = frame.f_back return path Quixote-2.7b2/quixote/wsgi.py0000664000175000017500000000256111326376510014423 0ustar nasnas""" QWIP: a simple yet functional Quixote/WSGI application-side adapter for Quixote 2.x. To create an application object, execute app_obj = QWIP(publisher) Authors: Mike Orr and Titus Brown . Last updated 2005-05-03. """ import sys from http_request import HTTPRequest from StringIO import StringIO ###### QWIP: WSGI COMPATIBILITY WRAPPER FOR QUIXOTE ##################### class QWIP: """I make a Quixote Publisher object look like a WSGI application.""" request_class = HTTPRequest def __init__(self, publisher): self.publisher = publisher def __call__(self, env, start_response): """I am called for each request.""" if env.get('wsgi.multithread') and not \ getattr(self.publisher, 'is_thread_safe', False): reason = "%r is not thread safe" % self.publisher raise AssertionError(reason) if 'REQUEST_URI' not in env: env['REQUEST_URI'] = env['SCRIPT_NAME'] + env['PATH_INFO'] input = env['wsgi.input'] request = self.request_class(input, env) response = self.publisher.process_request(request) status = "%03d %s" % (response.status_code, response.reason_phrase) headers = response.generate_headers() start_response(status, headers) return response.generate_body_chunks() # Iterable object. Quixote-2.7b2/tests/0000775000175000017500000000000011326377244012545 5ustar nasnasQuixote-2.7b2/tests/__init__.py0000664000175000017500000000053711114154100014636 0ustar nasnasimport qx_testlib import qx_testserver import twill url = None def setup(package): qx_testlib.cd_testdir() package.url = qx_testlib.run_server(qx_testserver.create_publisher) def teardown(package): qx_testlib.kill_server() qx_testlib.pop_testdir() def test(): twill.commands.go(url) twill.commands.find('hello, world') Quixote-2.7b2/tests/qx_testlib.py0000664000175000017500000000365011326376521015276 0ustar nasnasimport sys, subprocess import quixote from quixote.server.simple_server import run from StringIO import StringIO import os import socket import urllib _server_url = None testdir = os.path.dirname(__file__) print 'testdir is:', testdir sys.path.insert(0, os.path.abspath(os.path.join(testdir, '..'))) import twill def cd_testdir(): global cwd cwd = os.getcwd() os.chdir(testdir) def pop_testdir(): global cwd os.chdir(cwd) def execute_twill_script(filename, inp=None, initial_url=None): global testdir if inp: inp_fp = StringIO(inp) old, sys.stdin = sys.stdin, inp_fp scriptfile = os.path.join(testdir, filename) try: twill.execute_file(filename, initial_url=initial_url) finally: if inp: sys.stdin = old def run_server(create_fn, PORT=None): """ Run a Quixote simple_server on localhost:PORT with subprocess. All output is captured & thrown away. The parent process returns the URL on which the server is running. """ import time, tempfile global _server_url if PORT is None: PORT = int(os.environ.get('QX_TEST_PORT', '8080')) outfd = tempfile.mkstemp('quixote_tst')[0] print 'STARTING:', sys.executable, 'tests/qx_testserver.py', os.getcwd() process = subprocess.Popen([sys.executable, '-u', 'qx_testserver.py'], stderr=subprocess.STDOUT, stdout=outfd) time.sleep(1) result = process.poll() if result is not None: raise Exception("server is not running: return code %s" % (result,)) _server_url = 'http://localhost:%d/' % (PORT,) return _server_url def kill_server(): """ Kill the previously started Quixote server. """ global _server_url if _server_url != None: try: fp = urllib.urlopen('%sexit' % (_server_url,)) except: pass _server_url = None Quixote-2.7b2/tests/qx_testserver.py0000664000175000017500000000135011114154100016007 0ustar nasnas""" A simple test server for testing Quixote functionality. """ import os from quixote.publish import Publisher from quixote.directory import Directory def create_publisher(): "Create & return a test publisher entry" p = Publisher(TestServer()) p.is_thread_safe = True return p class TestServer(Directory): _q_exports = ['', 'exit'] def _q_index(self): return "hello, world" def exit(self): raise SystemExit if __name__ == '__main__': from quixote.server.simple_server import run port = int(os.environ.get('QX_TEST_PORT', '8080')) print 'starting qx_testserver on port %d.' % (port,) try: run(create_publisher, port=port) except KeyboardInterrupt: pass Quixote-2.7b2/tests/serve-via-wsgi.py0000664000175000017500000000031711114154100015743 0ustar nasnasimport quixote, qx_testserver from wsgiref.simple_server import make_server qx_testserver.create_publisher() wsgi_app = quixote.get_wsgi_app() httpd = make_server('', 8000, wsgi_app) httpd.serve_forever() Quixote-2.7b2/tests/test-qpy.py0000664000175000017500000000042411114154100014660 0ustar nasnasimport quixote.html try: import qpy except ImportError: qpy = None if qpy: def setup(): quixote.html.use_qpy() def test(): import quixote assert quixote.html.htmltext == qpy.h8 def teardown(): quixote.html.cleanup_qpy() Quixote-2.7b2/tests/test-wsgi.py0000664000175000017500000000130711114154100015021 0ustar nasnasimport sys import twill import quixote from qx_testserver import create_publisher class TestWSGI: def setup(self): wsgi_app = None x = sys.stdout # Quixote mangles sys.stdout; save. try: publisher = create_publisher() wsgi_app = quixote.get_wsgi_app() finally: sys.stdout = x # restore. twill.add_wsgi_intercept('localhost', 80, lambda:wsgi_app, '/qx_test') def teardown(self): twill.remove_wsgi_intercept('localhost', 80) quixote.cleanup() def test(self): twill.commands.go('http://localhost:80/qx_test/') twill.commands.find('hello, world') Quixote-2.7b2/ACKS.txt0000664000175000017500000000171011103004232012635 0ustar nasnasAcknowledgements ================ The Quixote developer team would like to thank everybody who contributed in any way, with code, hints, bug reports, ideas, moral support, endorsement, or even complaints. Listed in alphabetical order: David Ascher Anton Benard David Binger Titus Brown Oleg Broytmann Shalabh Chaturvedi David M. Cooke Jonathan Corbet David Creemer Herman Cuppens Michael Davidson Toby Dickenson Ray Drew Jim Dukarm Quinn Dunkan Robin Dunn Jon Dyte David Edwards Graham Fawcett Jim Fulton David Goodger Neal M. Holtz Kaweh Kazemi Shahms E. King Alexander J. Kozlovsky A.M. Kuchling (Quixote originator) Erno Kuusela Nicola Larosa Hamish Lawson Dryice Liu Roger Masse Patrick K. O'Brien Brendan T O'Connor Ed Overly Matt Patterson Paul Richardson Jeff Rush Neil Schemenauer (Quixote originator and BD) Jason Sibre Gregory P. Smith Mikhail Sobolev Daniele Varrazzo Johann Visagie Greg Ward (Quixote originator) The whole gang at the Zope Corporation Quixote-2.7b2/CHANGES.txt0000664000175000017500000002054411326377244013221 0ustar nasnasSummary of changes ================== v2.7b2 ------ Author: Neil Schemenauer Date: Fri Jan 22 13:35:21 2010 -0600 Update version to 2.7b2 Author: Neil Schemenauer Date: Fri Jan 22 13:32:44 2010 -0600 Use the StringIO module rather than cStringIO. cStringIO is gone in Python 3 and also does not handle unicode strings properly. Author: Neil Schemenauer Date: Fri Jan 22 13:29:46 2010 -0600 By default, set Cache-Control in addition to the Expires header. The Expires header is sufficient for HTTP 1.0 but for HTTP 1.1 we must add a must-revalidate directive. Clients and proxies are allowed to ignore Expires in certain cases and use stale pages (RFC 2616 sections 13.1.5 and 14.9.4). Author: Neil Schemenauer Date: Fri Jan 22 13:28:58 2010 -0600 Disable cimport module for Python >= 2.6. The current version of the cimport module does not support relative imports. Disable it for now. Author: Neil Schemenauer Date: Sun Dec 13 14:18:45 2009 -0600 Fix reference to compile_file function (fixes compile_dir function). v2.7b1 ------ Author: Neil Schemenauer Date: Mon Sep 7 00:41:44 2009 -0600 Update version for 2.7b1 release. Author: Neil Schemenauer Date: Mon Sep 7 00:42:51 2009 -0600 Add session iterator. Author: Neil Schemenauer Date: Wed Dec 3 14:41:05 2008 -0600 Don't use callable(). Author: Neil Schemenauer Date: Wed Dec 3 12:43:38 2008 -0600 Use __contains__ instead of has_key. Author: Neil Schemenauer Date: Wed Dec 3 12:41:18 2008 -0600 Use utf-8 as default encoding. Author: Neil Schemenauer Date: Fri Nov 28 23:00:40 2008 -0600 Use built-in set type. Author: Neil Schemenauer Date: Mon Sep 7 01:30:26 2009 -0600 Work around broken ihooks module in Python 2.6. Using the import hook is still the most convenient way of using PTL modules. Author: Neil Schemenauer Date: Sun Apr 12 10:57:06 2009 -0600 Remove spurious kwargs from WidgetDict.__init__. Author: Neil Schemenauer Date: Tue Jun 16 09:55:31 2009 -0600 Add options to sendmail so it can be used without a Quixote config. Also, remove broken encode() call since it can't handle Unicode properly as implemented. Author: Neil Schemenauer Date: Sun May 31 19:09:53 2009 -0600 Add SESSION_COOKIE_SECURE and SESSION_COOKIE_HTTPONLY. Based on a suggestion from Emmanuel Dreyfus , add the SESSION_COOKIE_SECURE and SESSION_COOKIE_HTTPONLY options. Setting them to true will cause the corresponding flag to be set on the session cookie. Author: Hamish Lawson Date: Mon Feb 2 10:04:04 2009 -0600 Check for other possible values of HTTPS. Currently HTTPRequest only checks whether the HTTPS environment variable has a value of 'on', but other possible positive values are '1' (as set by mod_wsgi) and 'yes'. Author: Neil Schemenauer Date: Tue Jan 6 20:16:39 2009 -0600 Avoid infinite redirect when PATH_INFO is empty. v2.6 ---- Author: Neil Schemenauer Date: Tue Nov 25 22:22:12 2008 -0600 Add quixote.ptl.compile_package function. The ihooks module is broken in Python 2.6 and will be gone in Python 3. Provide compile_package() as an alternative method of compiling PTL templates. Author: Neil Schemenauer Date: Sat Jul 5 17:51:20 2008 -0600 Prepare for v2.6 release. Remove CHANGES.txt file and generate it as necessary. Update version numbers and remove setup.py version check. Author: Neil Schemenauer Date: Sat Jul 5 17:50:14 2008 -0600 Improve README.txt. Add link to wiki. Add instructions on how to run the mini demo. Author: Neil Schemenauer Date: Sat Apr 5 20:05:33 2008 -0600 Use SCGIMount directive in documentation. Author: Neil Schemenauer Date: Sat Apr 5 15:15:40 2008 -0600 Remove a reference to _q_exception_handler. Author: Neil Schemenauer Date: Sat Feb 16 15:09:45 2008 -0600 Silence struck.pack warning. Silence struct.pack warning by forcing gzipped response CRC to be a positive integer. v2.5 ---- Author: Neil Schemenauer Date: Fri Nov 16 16:36:30 2007 -0600 Update CHANGES.txt for 2.5 release. Author: Neil Schemenauer Date: Fri Nov 16 16:35:33 2007 -0600 Fix PTL handling of __future__ statements. v2.5b1 ------ Author: Neil Schemenauer Date: Fri Apr 6 00:01:32 2007 -0600 Improve setup.py for 2.5b1 release. Author: Neil Schemenauer Date: Thu Apr 5 23:33:22 2007 -0600 Prepare for 2.5b1 release. Author: C. Titus Brown Date: Fri Feb 9 17:00:37 2007 -0600 Reorganize source Added some simple WSGI documentation; cleaned up test code a bit. Added test server status check. Moved the quixote package into the quixote/ subdirectory. Added nose+twill tests under tests/. Author: C. Titus Brown Date: Tue Jan 30 09:25:26 2007 -0600 Add WSGI and qpy support. Added quixote.html.use_qpy to switch Quixote to using qpy instead of htmltext (code contributed by Mike Orr). Added quixote.cleanup() to clear _publisher to support test fixtures. Added wsgi.py, quixote.get_wsgi_app() for WSGI support. Author: Neil Schemenauer Date: Tue Jan 30 09:23:25 2007 -0600 In scgi_server.py, make host default to 'localhost'. It seems better to default to a more secure setup. The previous code caused the server to listen on all interfaces by default. Author: Neil Schemenauer Date: Tue Jan 30 09:21:51 2007 -0600 Make setup.py URL point to quixote.ca site. Author: Neil Schemenauer Date: Thu Sep 7 10:59:51 2006 -0600 Add comment explaining why single-select options are required. Author: Neil Schemenauer Date: Wed Aug 23 20:39:49 2006 -0600 Allow setup.py to work without the "quixote" package. Also, improve the PyPI information. v2.5a1 ------ Author: Neil Schemenauer Date: Tue Aug 15 12:02:13 2006 -0600 Do version check only when building source distribution. Author: Neil Schemenauer Date: Tue Aug 8 20:43:03 2006 -0600 Prepare for 2.5a1 release. Author: Neil Schemenauer Date: Mon Aug 7 16:19:28 2006 -0600 Have setup.py check version numbers Make setup.py check that quixote.__version__ matches the one in CHANGES.txt. Also, stop distributing MANIFEST since it is a pain to ensure that it is up-to-date. Author: Neil Schemenauer Date: Mon Aug 7 16:01:12 2006 -0600 Make PTL work with Python 2.5. Author: Neil Schemenauer Date: Mon Aug 7 15:53:48 2006 -0600 Don't try to process non-ReST text files with rst2html Author: Neil Schemenauer Date: Mon Aug 7 15:51:52 2006 -0600 Make MANIFEST.in more accurate. Author: Neil Schemenauer Date: Fri May 19 11:55:30 2006 -0600 Have simple_server.py call server_close(). Use a try/finally block to ensure that server_close() is called by simple_server.py (as requested by Michele Simionato). Author: Neil Schemenauer Date: Fri May 19 11:54:24 2006 -0600 Remove empty "name" attribute from an input field. Remove the empty "name" attribute from the noscript input field created by OptionSelectWidget. This change does not seem to effect behavior and makes Tidy happier. Author: Neil Schemenauer Date: Fri May 19 11:51:55 2006 -0600 Add get_size() method to the Upload class. Author: Neil Schemenauer Date: Thu Mar 16 19:48:55 2006 -0700 Update README and LICENSE. Author: Neil Schemenauer Date: Thu Mar 16 13:18:21 2006 -0700 Remove Subversion keywords. v2.4 ---- Author: Neil Schemenauer Date: Wed Mar 15 17:58:21 2006 -0700 Import Quixote 2.4. Quixote-2.7b2/LICENSE.txt0000664000175000017500000000256411103004232013206 0ustar nasnasThis version of Quixote is derived from Quixote 2.4, released by CNRI. See doc/LICENSE_24.txt for the licensing terms of that release. Changes made since that release are summarized in the CHANGES.txt file along with a list of authors. Those changes are made available under the following terms (commonly known as the MIT/X license). Copyright (c) the Quixote developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Quixote-2.7b2/MANIFEST.in0000664000175000017500000000030511103004232013110 0ustar nasnasglobal-include *.py *.ptl include *.txt MANIFEST.in TODO include doc/*.txt doc/*.css doc/Makefile recursive-include doc *.html include demo/*.cgi demo/*.conf demo/*.sh include src/*.c src/Makefile Quixote-2.7b2/README.txt0000664000175000017500000000324711114154100013062 0ustar nasnasQuixote ======= Quixote is a framework for developing Web applications in Python. The target is web applications that are developed and maintained by Python programmers. See http://quixote.ca/users for a list of some applications using Quixote. For installation instructions, see the doc/INSTALL.txt file. For the impatient, install the package using setup.py and then run: python quixote/demo/mini_demo.py Note that you can copy the mini_demo.py somewhere and use it as a starting point for your application if you like. See the Quixote wiki page for more hints. Documentation is available in the doc/ directory and on the Quixote web site. Quixote includes PTL, the Python Template Language for producing HTML with Python code. Note that the use of PTL is not required in Quixote applications. Details about PTL are provided in doc/PTL.txt. Authors, copyright, and license =============================== Quixote is copyrighted and made available under open source licensing terms. See LICENSE.txt for the details. The ACKS.txt file lists people who have assisted in the development of Quixote. The CHANGES.txt file summarizes the changes made since version 2.4 and lists the authors of those changes. Availability, home page, and mailing lists ========================================== The Quixote home page is: http://quixote.ca/ There is wiki at: http://quixote.ca/qx/ Discussion of Quixote occurs on the quixote-users mailing list: http://mail.mems-exchange.org/mailman/listinfo/quixote-users/ The source code is managed using git. You can checkout a copy using the command: git clone http://quixote.ca/src/quixote.git Quixote-2.7b2/TODO0000664000175000017500000000137211103004232012047 0ustar nasnas* Extend HTTPRequest to support single/multiple-valued fields. * Make bare return statements inside of PTL templates work as expected. * Allow __init__.ptl files to be used as package markers. It looks like something is wrong with the way ihooks handles __init__ modules. * Logging doesn't work with CGI scripts (something about our log-opening code depends on how fastcgi.py fiddles stdout). * For OpenBSD: fcgi.py should catch SIGTERM and, umm, do something. (Terminate the process?) Otherwise, the FastCGI process can no longer accept() on its socket. (Reported by Robin Wöhler , 2002/08/02.) * For Mac OS X: _startup() in fcgi.py doesn't work for some reason on OS X. Figure out why and fix it (or kludge around it). Quixote-2.7b2/setup.py0000664000175000017500000000460511114154100013075 0ustar nasnas#!/usr/bin/env python #try: # from setuptools import setup #except ImportError: # print '(WARNING: importing distutils, not setuptools!)' # from distutils.core import setup # Setup script for Quixote import sys import os from distutils import core from distutils.extension import Extension from quixote.ptl.qx_distutils import qx_build_py from quixote import __version__ # a fast htmltext type htmltext = Extension(name="quixote.html._c_htmltext", sources=["quixote/html/_c_htmltext.c"]) # faster import hook for PTL modules cimport = Extension(name="quixote.ptl.cimport", sources=["quixote/ptl/cimport.c"]) kw = {'name': "Quixote", 'version': __version__, 'description': "A small and flexible Python Web application framework", 'author': "The Quixote developers", 'author_email': "webmaster@quixote.ca", 'url': "http://www.quixote.ca/", 'license': "DFSG approved (see LICENSE.txt)", 'package_dir': {'quixote': 'quixote'}, 'packages': ['quixote', 'quixote.demo', 'quixote.form', 'quixote.html', 'quixote.ptl', 'quixote.server'], 'ext_modules': [], 'cmdclass': {'build_py': qx_build_py}, # 'test_suite' : 'nose.collector' } build_extensions = sys.platform != 'win32' if build_extensions: # The _c_htmltext module requires Python 2.2 features. if sys.hexversion >= 0x20200a1: kw['ext_modules'].append(htmltext) kw['ext_modules'].append(cimport) # If we're running Python 2.3, add extra information if hasattr(core, 'setup_keywords'): if 'classifiers' in core.setup_keywords: kw['classifiers'] = ['Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'License :: DFSG approved', 'Intended Audience :: Developers', 'Operating System :: Unix', 'Operating System :: Microsoft :: Windows', 'Operating System :: MacOS :: MacOS X', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ] if 'download_url' in core.setup_keywords: kw['download_url'] = ('http://quixote.ca/releases/' 'Quixote-%s.tar.gz' % kw['version']) if 'url' in core.setup_keywords: kw['url'] = 'http://www.quixote.ca/' if 'platforms' in core.setup_keywords: kw['platforms'] = 'Most' core.setup(**kw) Quixote-2.7b2/PKG-INFO0000664000175000017500000000133511326377244012502 0ustar nasnasMetadata-Version: 1.0 Name: Quixote Version: 2.7b2 Summary: A small and flexible Python Web application framework Home-page: http://www.quixote.ca/ Author: The Quixote developers Author-email: webmaster@quixote.ca License: DFSG approved (see LICENSE.txt) Download-URL: http://quixote.ca/releases/Quixote-2.7b2.tar.gz Description: UNKNOWN Platform: Most Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: License :: DFSG approved Classifier: Intended Audience :: Developers Classifier: Operating System :: Unix Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: MacOS :: MacOS X Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content