# HG changeset patch # User k0s # Date 1260303208 18000 # Node ID 3c3522ce6e3a72bf4319067dd6bbb2a7e63c57cd initial import of martINI from https://svn.openplans.org/svn/standalone/martINI/ diff -r 000000000000 -r 3c3522ce6e3a example/conf/foo.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/example/conf/foo.ini Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,3 @@ +[foo] +bar = bar +baz = baz \ No newline at end of file diff -r 000000000000 -r 3c3522ce6e3a example/conf/subdir/bar.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/example/conf/subdir/bar.ini Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,2 @@ +[bar] +barbar = jarjar \ No newline at end of file diff -r 000000000000 -r 3c3522ce6e3a example/example.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/example/example.ini Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,8 @@ +[bar] +bar = barber +forg = boo +vashti = buny + +[foofoofoo] +bar = fleem +forg = vogel diff -r 000000000000 -r 3c3522ce6e3a martini/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/martini/__init__.py Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,1 @@ +# diff -r 000000000000 -r 3c3522ce6e3a martini/config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/martini/config.py Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,144 @@ +#!/usr/bin/env python + +import os +import sys +import urllib2 + +from ConfigParser import ConfigParser +from ConfigParser import InterpolationMissingOptionError +from ConfigParser import MissingSectionHeaderError +from ConfigParser import NoOptionError +from StringIO import StringIO + +def file_pointer(resource): + """returns a file-like object given a string""" + # XXX could go in utils.py + + if not isinstance(resource, basestring): + # assume resource is already a file-like object + return resource + + if os.path.exists(resource): + return file(resource) + if sum([resource.startswith(http) for http in 'http://', 'https://']): + return urllib2.urlopen(resource) + return StringIO(resource) + + +class ConfigMunger(ConfigParser): + """combine configuration from .ini files""" + + def __init__(self, *conf, **kw): + ConfigParser.__init__(self, kw.get('defaults',{})) + self.read(*conf) + + def __getitem__(self, section): + """ + return an object with __getitem__ defined appropriately + to allow referencing like self['foo']['bar'] + """ + return dict(self.items(section)) + + def get(self, section, option, default=None, raw=False, vars=None): + try: + value = ConfigParser.get(self, section, option, raw, vars) + except NoOptionError: + return default + return value + + def set(self, section, option, value): + if section not in self.sections(): + self.add_section(section) + ConfigParser.set(self, section, option, value) + + def move_section(self, section, newname): + if self.has_section(section): + _section = self[section] + self.remove_section(section) + else: + _section = {} + self.read({newname: _section}) + + def dict(self): + """return a dictionary of dictionaries; + the outer with keys of section names; + the inner with keys, values of the section""" + return dict([(section, self[section]) + for section in self.sections()]) + + def read(self, *ini): + for _ini in ini: + if isinstance(_ini, dict): + for section, contents in _ini.items(): + for option, value in contents.items(): + self.set(section, option, value) + elif isinstance(_ini, list) or isinstance(_ini, tuple): + + # ensure list or tuple of 3-tuples + assert len([option for option in _ini + if isinstance(option, tuple) + and len(option) == 3]) + + for section, option, value in _ini: + self.set(section, option, value) + else: + fp = file_pointer(_ini) + try: + self.readfp(fp) + except MissingSectionHeaderError: + fp.seek(0) + fp = StringIO("[DEFAULTS]\n" + fp.read()) + self.readfp(fp) + + def missing(self): + """returns missing variable names""" + missing = set() + + for section in self.sections(): + for key, val in self.items(section, raw=True): + try: + self.get(section, key) + except InterpolationMissingOptionError, e: + missing.add(e.reference) + return missing + + def tuples(self): + """ + return options in format appropriate to trac: + [ (section, option, value) ] + """ + options = [] + for section in self.sections(): + options.extend([(section,) + item + for item in self.items(section)]) + return options + + def write(self, fp=sys.stdout, raw=False, sorted=True, vars=None): + sections = self.sections() + if sorted: + sections.sort() + + for section in sections: + print >> fp, '[%s]' % section + options = self.options(section) + if sorted: + options.sort() + for option in options: + print >> fp, "%s = %s" % (option, self.get(section, option, raw, vars)) + if section != sections[-1]: + print >> fp + +if __name__ == '__main__': + import sys + from optparse import OptionParser + parser = OptionParser() + parser.add_option('--missing', action="store_true", default=False, + help="list missing template variables") + munger = ConfigMunger() + options, args = parser.parse_args() + munger.read(*args) + if options.missing: + for missing in munger.missing(): + print missing + else: + munger.write(sys.stdout) diff -r 000000000000 -r 3c3522ce6e3a martini/main.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/martini/main.py Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,149 @@ +#!/usr/bin/env python + +usage = "martini file1 [file2] [...] --section1 option1=value1 option2=value2 --section2 option3=value3" + +import config +import os +import sys + +sep = '--' # section separator + +def parse_args(*args): + """ + convert command line options to files, sections, and options + returns a tuple: + ( [files], { 'section': { 'option': 'value', 'option2': 'value2' } } ) + """ + + # find the files + # XXX note this prohibits files starting with -- + index = None + for index, value in enumerate(args): + if value.startswith(sep): + break + else: + return (args, []) + + files = args[:index] + args = args[index:] + + # find the sections + ini = [] + for arg in args: + if arg.startswith(sep): + arg = arg[len(sep):] + assert arg + section = [] + ini.append((arg, section)) + else: + section.append(arg) + + return (files, ini) + +def parse_options(*args): + files, sections = parse_args(*args) + ini = {} + for section, options in sections: + ini[section] = {} + for option in options: + key, value = option.split('=', 1) + ini[section][key] = value + + return (files, ini) + +def set(args=None): + + # process arguments + if args is None: + args = sys.argv[1:] + files, sections = parse_options(*args) + + # display usage information + if not files: + print 'Usage:' + print usage + sys.exit(0) + + # process the files + for f in files: + if f == '-': + fp = sys.stdin + else: + fp = file(f) + munger = config.ConfigMunger(fp, sections) + + if f == '-': + fp = sys.stdout + else: + fp.close() + fp = file(f, "w") + munger.write(fp=fp) + +def get(args=None): + + # process arguments + if args is None: + args = sys.argv[1:] + files, sections = parse_args(*args) + + # display usage information + if not files: + print 'Usage:' + print usage + sys.exit(0) + + # process the files + for f in files: + if f == '-': + fp = sys.stdin + else: + fp = file(f) + munger = config.ConfigMunger(fp) + for section, options in sections: + if section in munger.sections(): + if options: + for option in options: + value = munger.get(section, option) + if value is not None: + print value + else: + config.ConfigMunger({section: munger[section]}).write() + +def delete(args=None): + + # process arguments + if args is None: + args = sys.argv[1:] + files, sections = parse_args(*args) + + # display usage information + if not files: + print 'Usage:' + print usage + sys.exit(0) + + # process the files + for f in files: + if f == '-': + fp = sys.stdin + else: + fp = file(f) + conf = config.ConfigMunger(fp).dict() + for section, options in sections: + if section in conf: + if options: + for option in options: + if option in conf[section]: + conf[section].pop(option) + else: + conf.pop(section) + if f == '-': + fp = sys.stdout + else: + fp.close() + fp = file(f, 'w') + config.ConfigMunger(conf).write(fp) + + +if __name__ == '__main__': + set(sys.argv[1:]) diff -r 000000000000 -r 3c3522ce6e3a martini/static/jquery.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/martini/static/jquery.js Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,4376 @@ +/*! + * jQuery JavaScript Library v1.3.2 + * http://jquery.com/ + * + * Copyright (c) 2009 John Resig + * Dual licensed under the MIT and GPL licenses. + * http://docs.jquery.com/License + * + * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) + * Revision: 6246 + */ +(function(){ + +var + // Will speed up references to window, and allows munging its name. + window = this, + // Will speed up references to undefined, and allows munging its name. + undefined, + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + // Map over the $ in case of overwrite + _$ = window.$, + + jQuery = window.jQuery = window.$ = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context ); + }, + + // A simple way to check for HTML strings or ID strings + // (both of which we optimize for) + quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/, + // Is it a simple selector + isSimple = /^.[^:#\[\.,]*$/; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + // Make sure that a selection was provided + selector = selector || document; + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this[0] = selector; + this.length = 1; + this.context = selector; + return this; + } + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + var match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) + selector = jQuery.clean( [ match[1] ], context ); + + // HANDLE: $("#id") + else { + var elem = document.getElementById( match[3] ); + + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem && elem.id != match[3] ) + return jQuery().find( selector ); + + // Otherwise, we inject the element directly into the jQuery object + var ret = jQuery( elem || [] ); + ret.context = document; + ret.selector = selector; + return ret; + } + + // HANDLE: $(expr, [context]) + // (which is just equivalent to: $(content).find(expr) + } else + return jQuery( context ).find( selector ); + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) + return jQuery( document ).ready( selector ); + + // Make sure that old selector state is passed along + if ( selector.selector && selector.context ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return this.setArray(jQuery.isArray( selector ) ? + selector : + jQuery.makeArray(selector)); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.3.2", + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num === undefined ? + + // Return a 'clean' array + Array.prototype.slice.call( this ) : + + // Return just the object + this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = jQuery( elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) + ret.selector = this.selector + (this.selector ? " " : "") + selector; + else if ( name ) + ret.selector = this.selector + "." + name + "(" + selector + ")"; + + // Return the newly-formed element set + return ret; + }, + + // Force the current matched set of elements to become + // the specified array of elements (destroying the stack in the process) + // You should use pushStack() in order to do this, but maintain the stack + setArray: function( elems ) { + // Resetting the length to 0, then using the native Array push + // is a super-fast way to populate an object with array-like properties + this.length = 0; + Array.prototype.push.apply( this, elems ); + + return this; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem && elem.jquery ? elem[0] : elem + , this ); + }, + + attr: function( name, value, type ) { + var options = name; + + // Look for the case where we're accessing a style value + if ( typeof name === "string" ) + if ( value === undefined ) + return this[0] && jQuery[ type || "attr" ]( this[0], name ); + + else { + options = {}; + options[ name ] = value; + } + + // Check to see if we're setting style values + return this.each(function(i){ + // Set all the styles + for ( name in options ) + jQuery.attr( + type ? + this.style : + this, + name, jQuery.prop( this, options[ name ], type, i, name ) + ); + }); + }, + + css: function( key, value ) { + // ignore negative width and height values + if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) + value = undefined; + return this.attr( key, value, "curCSS" ); + }, + + text: function( text ) { + if ( typeof text !== "object" && text != null ) + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + + var ret = ""; + + jQuery.each( text || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + ret += this.nodeType != 1 ? + this.nodeValue : + jQuery.fn.text( [ this ] ); + }); + }); + + return ret; + }, + + wrapAll: function( html ) { + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).clone(); + + if ( this[0].parentNode ) + wrap.insertBefore( this[0] ); + + wrap.map(function(){ + var elem = this; + + while ( elem.firstChild ) + elem = elem.firstChild; + + return elem; + }).append(this); + } + + return this; + }, + + wrapInner: function( html ) { + return this.each(function(){ + jQuery( this ).contents().wrapAll( html ); + }); + }, + + wrap: function( html ) { + return this.each(function(){ + jQuery( this ).wrapAll( html ); + }); + }, + + append: function() { + return this.domManip(arguments, true, function(elem){ + if (this.nodeType == 1) + this.appendChild( elem ); + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function(elem){ + if (this.nodeType == 1) + this.insertBefore( elem, this.firstChild ); + }); + }, + + before: function() { + return this.domManip(arguments, false, function(elem){ + this.parentNode.insertBefore( elem, this ); + }); + }, + + after: function() { + return this.domManip(arguments, false, function(elem){ + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + }, + + end: function() { + return this.prevObject || jQuery( [] ); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: [].push, + sort: [].sort, + splice: [].splice, + + find: function( selector ) { + if ( this.length === 1 ) { + var ret = this.pushStack( [], "find", selector ); + ret.length = 0; + jQuery.find( selector, this[0], ret ); + return ret; + } else { + return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){ + return jQuery.find( selector, elem ); + })), "find", selector ); + } + }, + + clone: function( events ) { + // Do the clone + var ret = this.map(function(){ + if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // In order to get around this, we use innerHTML. + // Unfortunately, this means some modifications to + // attributes in IE that are actually only stored + // as properties will not be copied (such as the + // the name attribute on an input). + var html = this.outerHTML; + if ( !html ) { + var div = this.ownerDocument.createElement("div"); + div.appendChild( this.cloneNode(true) ); + html = div.innerHTML; + } + + return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; + } else + return this.cloneNode(true); + }); + + // Copy the events from the original to the clone + if ( events === true ) { + var orig = this.find("*").andSelf(), i = 0; + + ret.find("*").andSelf().each(function(){ + if ( this.nodeName !== orig[i].nodeName ) + return; + + var events = jQuery.data( orig[i], "events" ); + + for ( var type in events ) { + for ( var handler in events[ type ] ) { + jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); + } + } + + i++; + }); + } + + // Return the cloned set + return ret; + }, + + filter: function( selector ) { + return this.pushStack( + jQuery.isFunction( selector ) && + jQuery.grep(this, function(elem, i){ + return selector.call( elem, i ); + }) || + + jQuery.multiFilter( selector, jQuery.grep(this, function(elem){ + return elem.nodeType === 1; + }) ), "filter", selector ); + }, + + closest: function( selector ) { + var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null, + closer = 0; + + return this.map(function(){ + var cur = this; + while ( cur && cur.ownerDocument ) { + if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) { + jQuery.data(cur, "closest", closer); + return cur; + } + cur = cur.parentNode; + closer++; + } + }); + }, + + not: function( selector ) { + if ( typeof selector === "string" ) + // test special case where just one selector is passed in + if ( isSimple.test( selector ) ) + return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector ); + else + selector = jQuery.multiFilter( selector, this ); + + var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; + return this.filter(function() { + return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; + }); + }, + + add: function( selector ) { + return this.pushStack( jQuery.unique( jQuery.merge( + this.get(), + typeof selector === "string" ? + jQuery( selector ) : + jQuery.makeArray( selector ) + ))); + }, + + is: function( selector ) { + return !!selector && jQuery.multiFilter( selector, this ).length > 0; + }, + + hasClass: function( selector ) { + return !!selector && this.is( "." + selector ); + }, + + val: function( value ) { + if ( value === undefined ) { + var elem = this[0]; + + if ( elem ) { + if( jQuery.nodeName( elem, 'option' ) ) + return (elem.attributes.value || {}).specified ? elem.value : elem.text; + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type == "select-one"; + + // Nothing was selected + if ( index < 0 ) + return null; + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + if ( option.selected ) { + // Get the specifc value for the option + value = jQuery(option).val(); + + // We don't need an array for one selects + if ( one ) + return value; + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + } + + // Everything else, we just grab the value + return (elem.value || "").replace(/\r/g, ""); + + } + + return undefined; + } + + if ( typeof value === "number" ) + value += ''; + + return this.each(function(){ + if ( this.nodeType != 1 ) + return; + + if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) ) + this.checked = (jQuery.inArray(this.value, value) >= 0 || + jQuery.inArray(this.name, value) >= 0); + + else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(value); + + jQuery( "option", this ).each(function(){ + this.selected = (jQuery.inArray( this.value, values ) >= 0 || + jQuery.inArray( this.text, values ) >= 0); + }); + + if ( !values.length ) + this.selectedIndex = -1; + + } else + this.value = value; + }); + }, + + html: function( value ) { + return value === undefined ? + (this[0] ? + this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") : + null) : + this.empty().append( value ); + }, + + replaceWith: function( value ) { + return this.after( value ).remove(); + }, + + eq: function( i ) { + return this.slice( i, +i + 1 ); + }, + + slice: function() { + return this.pushStack( Array.prototype.slice.apply( this, arguments ), + "slice", Array.prototype.slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function(elem, i){ + return callback.call( elem, i, elem ); + })); + }, + + andSelf: function() { + return this.add( this.prevObject ); + }, + + domManip: function( args, table, callback ) { + if ( this[0] ) { + var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), + scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ), + first = fragment.firstChild; + + if ( first ) + for ( var i = 0, l = this.length; i < l; i++ ) + callback.call( root(this[i], first), this.length > 1 || i > 0 ? + fragment.cloneNode(true) : fragment ); + + if ( scripts ) + jQuery.each( scripts, evalScript ); + } + + return this; + + function root( elem, cur ) { + return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ? + (elem.getElementsByTagName("tbody")[0] || + elem.appendChild(elem.ownerDocument.createElement("tbody"))) : + elem; + } + } +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +function evalScript( i, elem ) { + if ( elem.src ) + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + + else + jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + + if ( elem.parentNode ) + elem.parentNode.removeChild( elem ); +} + +function now(){ + return +new Date; +} + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) + target = {}; + + // extend jQuery itself if only one argument is passed + if ( length == i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) + // Extend the base object + for ( var name in options ) { + var src = target[ name ], copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) + continue; + + // Recurse if we're merging object values + if ( deep && copy && typeof copy === "object" && !copy.nodeType ) + target[ name ] = jQuery.extend( deep, + // Never move original objects, clone them + src || ( copy.length != null ? [ ] : { } ) + , copy ); + + // Don't bring in undefined values + else if ( copy !== undefined ) + target[ name ] = copy; + + } + + // Return the modified object + return target; +}; + +// exclude the following css properties to add px +var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, + // cache defaultView + defaultView = document.defaultView || {}, + toString = Object.prototype.toString; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) + window.jQuery = _jQuery; + + return jQuery; + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return toString.call(obj) === "[object Function]"; + }, + + isArray: function( obj ) { + return toString.call(obj) === "[object Array]"; + }, + + // check if an element is in a (or is an) XML document + isXMLDoc: function( elem ) { + return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || + !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument ); + }, + + // Evalulates a script in a global context + globalEval: function( data ) { + if ( data && /\S/.test(data) ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + if ( jQuery.support.scriptEval ) + script.appendChild( document.createTextNode( data ) ); + else + script.text = data; + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, length = object.length; + + if ( args ) { + if ( length === undefined ) { + for ( name in object ) + if ( callback.apply( object[ name ], args ) === false ) + break; + } else + for ( ; i < length; ) + if ( callback.apply( object[ i++ ], args ) === false ) + break; + + // A special, fast, case for the most common use of each + } else { + if ( length === undefined ) { + for ( name in object ) + if ( callback.call( object[ name ], name, object[ name ] ) === false ) + break; + } else + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} + } + + return object; + }, + + prop: function( elem, value, type, i, name ) { + // Handle executable functions + if ( jQuery.isFunction( value ) ) + value = value.call( elem, i ); + + // Handle passing in a number to a CSS property + return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, classNames ) { + jQuery.each((classNames || "").split(/\s+/), function(i, className){ + if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) + elem.className += (elem.className ? " " : "") + className; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, classNames ) { + if (elem.nodeType == 1) + elem.className = classNames !== undefined ? + jQuery.grep(elem.className.split(/\s+/), function(className){ + return !jQuery.className.has( classNames, className ); + }).join(" ") : + ""; + }, + + // internal only, use hasClass("class") + has: function( elem, className ) { + return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( var name in options ) + elem.style[ name ] = old[ name ]; + }, + + css: function( elem, name, force, extra ) { + if ( name == "width" || name == "height" ) { + var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; + + function getWH() { + val = name == "width" ? elem.offsetWidth : elem.offsetHeight; + + if ( extra === "border" ) + return; + + jQuery.each( which, function() { + if ( !extra ) + val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; + if ( extra === "margin" ) + val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0; + else + val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; + }); + } + + if ( elem.offsetWidth !== 0 ) + getWH(); + else + jQuery.swap( elem, props, getWH ); + + return Math.max(0, Math.round(val)); + } + + return jQuery.curCSS( elem, name, force ); + }, + + curCSS: function( elem, name, force ) { + var ret, style = elem.style; + + // We need to handle opacity special in IE + if ( name == "opacity" && !jQuery.support.opacity ) { + ret = jQuery.attr( style, "opacity" ); + + return ret == "" ? + "1" : + ret; + } + + // Make sure we're using the right name for getting the float value + if ( name.match( /float/i ) ) + name = styleFloat; + + if ( !force && style && style[ name ] ) + ret = style[ name ]; + + else if ( defaultView.getComputedStyle ) { + + // Only "float" is needed here + if ( name.match( /float/i ) ) + name = "float"; + + name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); + + var computedStyle = defaultView.getComputedStyle( elem, null ); + + if ( computedStyle ) + ret = computedStyle.getPropertyValue( name ); + + // We should always get a number back from opacity + if ( name == "opacity" && ret == "" ) + ret = "1"; + + } else if ( elem.currentStyle ) { + var camelCase = name.replace(/\-(\w)/g, function(all, letter){ + return letter.toUpperCase(); + }); + + ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { + // Remember the original values + var left = style.left, rsLeft = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + style.left = ret || 0; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + elem.runtimeStyle.left = rsLeft; + } + } + + return ret; + }, + + clean: function( elems, context, fragment ) { + context = context || document; + + // !context.createElement fails in IE with an error but returns typeof 'object' + if ( typeof context.createElement === "undefined" ) + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) { + var match = /^<(\w+)\s*\/?>$/.exec(elems[0]); + if ( match ) + return [ context.createElement( match[1] ) ]; + } + + var ret = [], scripts = [], div = context.createElement("div"); + + jQuery.each(elems, function(i, elem){ + if ( typeof elem === "number" ) + elem += ''; + + if ( !elem ) + return; + + // Convert html string into DOM nodes + if ( typeof elem === "string" ) { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ + return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? + all : + front + ">"; + }); + + // Trim whitespace, otherwise indexOf won't work as expected + var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase(); + + var wrap = + // option or optgroup + !tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [ 1, "", "
" ] || + + !tags.indexOf("", "" ] || + + // matched above + (!tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + // IE can't serialize and + + + + + + + + + + + + + + + + + + + +
${column}
${section}${sections[section].get(column)}
+ + + diff -r 000000000000 -r 3c3522ce6e3a martini/tests/__init__.py diff -r 000000000000 -r 3c3522ce6e3a martini/tests/test.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/martini/tests/test.py Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import doctest +import os + +def run_tests(): + directory = os.path.dirname(os.path.abspath(__file__)) + tests = [ test for test in os.listdir(directory) if test.endswith('.txt') ] + + for test in tests: + doctest.testfile(test) + +if __name__ == '__main__': + run_tests() diff -r 000000000000 -r 3c3522ce6e3a martini/tests/test_config.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/martini/tests/test_config.txt Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,102 @@ +Test martini.config +===================== + +The obligatory imports: + + >>> import sys + >>> from pprint import pprint + >>> from martini.config import ConfigMunger + >>> from ConfigParser import InterpolationMissingOptionError + >>> from StringIO import StringIO + +Make the munger and some .ini files: + + >>> munger = ConfigMunger() + >>> foo = StringIO("[foo]\nbar = fleem") + +Test basic reading + writing functionality: + + >>> munger.read(foo) + >>> munger.write(sys.stdout) + [foo] + bar = fleem + +Test overriding: + + >>> munger.read("[foo]\nblah = florg\nbar=thorg") + >>> munger.write(sys.stdout) + [foo] + bar = thorg + blah = florg + +Test variable interpolation: + + >>> munger.read("[variable]\nvar=%(value)s") + >>> buffer = StringIO() + >>> try: + ... munger.write(buffer) + ... except InterpolationMissingOptionError, e: + ... print e.reference + value + >>> munger.write(sys.stdout, vars=dict(value='interpolated')) + [foo] + bar = thorg + blah = florg + + [variable] + var = interpolated + +Testing dict-like functionality: + + >>> munger = ConfigMunger() + >>> munger.read("[foo]\nblah = florg\nbar=thorg") + >>> munger['foo']['bar'] + 'thorg' + >>> pprint(munger.dict()) + {'foo': {'bar': 'thorg', 'blah': 'florg'}} + >>> pprint(munger['foo']) + {'bar': 'thorg', 'blah': 'florg'} + +You can read from a dictionary too: + + >>> munger = ConfigMunger() + >>> munger.read({'foo': {'bar': 'baz'}}) + >>> munger['foo']['bar'] + 'baz' + >>> munger.write(sys.stdout) + [foo] + bar = baz + +Test the ability to parse sectionless .ini files: + + >>> munger = ConfigMunger() + >>> munger.read("foo = bar\nbaz=fleem") + >>> munger.sections() + ['DEFAULTS'] + >>> munger['DEFAULTS'] + {'foo': 'bar', 'baz': 'fleem'} + >>> munger.write(sys.stdout) + [DEFAULTS] + baz = fleem + foo = bar + >>> munger = ConfigMunger() + >>> munger.read("foo = bar\n\n[foo]\nbaz=fleem") + >>> sorted(munger.sections()) + ['DEFAULTS', 'foo'] + >>> munger['DEFAULTS']['foo'] + 'bar' + >>> munger['foo']['baz'] + 'fleem' + +Move a secton around: + + >>> munger.rename_section('foo', 'oof') + >>> sorted(munger.sections()) + ['DEFAULTS', 'oof'] + >>> munger['oof'] + {'baz': 'fleem'} + +Test the ability to parse multi-line .ini files [TODO]: + + >>> munger = ConfigMunger() + >>> # munger.read("[Jeff Hammel]\naddress = 639 W. 173 St.\nApt. 11D\nNew York, NY 10032") diff -r 000000000000 -r 3c3522ce6e3a martini/utils.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/martini/utils.py Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,13 @@ +def getlist(string, separator=','): + if not string: + return [] + _list = [ i.strip() for i in string.split(',') ] + if not _list: + return _list + if not _list[-1]: + return _list[:-1] + return _list + +def getbool(string): + return string.strip().lower() == 'true' + diff -r 000000000000 -r 3c3522ce6e3a martini/web.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/martini/web.py Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,206 @@ +""" +a view with webob + genshi for editing and viewing .ini files +""" +import os +import re + +from genshi.template import TemplateLoader +from martini.config import ConfigMunger +from martini.utils import getbool +from martini.utils import getlist +from paste.httpexceptions import HTTPExceptionHandler +from paste.urlparser import StaticURLParser +from pkg_resources import resource_filename +from webob import Request, Response, exc + +class MartiniWebView(object): + + ### class level variables + defaults = { 'auto_reload': 'True', + 'files': None, + 'directories': None } + + def __init__(self, **kw): + + # set instance parameters from kw and defaults + for key in self.defaults: + setattr(self, key, kw.get(key, self.defaults[key])) + self.auto_reload = getbool(self.auto_reload) + + # files + self.files = getlist(self.files) + if self.files: + assert [ f for f in self.files if os.path.isabs(f) ] + self.files = dict([(os.path.basename(f), f) + for f in self.files]) + else: + self.files = {} + + # directories + self.directories = getlist(self.directories) + if self.directories: + assert [ d for d in self.directories if os.path.isabs(d) ] + if len(self.directories) > 1 or self.files: + # namespace directories + self.directories = dict([(os.path.basename(d), d) + for d in self.directories]) + else: + # don't namespace a single directory + self.directories = { '': self.directories[0] } + else: + self.directories = {} + + # have to have something to serve! + assert self.files or self.directories + + # methods to respond to + self.response_functions = { 'GET': self.get, + 'POST': self.post, + } + + # template loader + templates_dir = resource_filename(__name__, 'templates') + self.loader = TemplateLoader(templates_dir, + auto_reload=self.auto_reload) + + # fileserver + self.fileserver = StaticURLParser(resource_filename(__name__, 'static')) + + def file(self, path): + path = path.strip('/') + if not path: + return None + f = self.files.get(path, None) + if f is None: + if path in self.directories: + return self.directories[path] + if '/' in path: + path = path.split('/', 1)[-1] + for d in self.directories.values(): + filename = os.path.join(d, path) + if os.path.exists(filename): + return filename + else: + return f + + ### methods dealing with HTTP + def __call__(self, environ, start_response): + request = Request(environ) + + if request.path_info.strip('/') and os.path.exists(os.path.join(resource_filename(__name__, 'static'), request.path_info.strip('/'))): + return self.fileserver(environ, start_response) + + if request.path_info.endswith('/'): + if request.path_info.strip('/'): + raise exc.HTTPFound(location=request.path_info.rstrip('/')) + else: + if request.path_info != '/': + raise exc.HTTPFound(location='/') + res = self.make_response(request) + return res(environ, start_response) + + def make_response(self, request): + return self.response_functions.get(request.method, self.error)(request) + + def get_response(self, text, content_type='text/html'): + """make an HTTP response from text""" + res = Response(content_type=content_type, body=text) + return res + + def get(self, request): + """ + return response to a GET requst + """ + + # index of all resources + if not request.path_info.strip('/'): + template = self.loader.load('index.html') + items = self.files.keys() + self.directories.keys() + res = template.generate(request=request, items=items).render('html', doctype='html') + return self.get_response(res) + + # get the file + f = self.file(request.path_info) + if not f: + raise exc.HTTPNotFound() + + # index page of a directory + if os.path.isdir(f): + template = self.loader.load('index.html') + items = os.listdir(f) + res = template.generate(request=request, items=items).render('html', doctype='html') + return self.get_response(res) + + # get configuration + conf = ConfigMunger(f) + + # render the template + template = self.loader.load('table.html') + res = template.generate(request=request, sections=conf.dict()).render('html', doctype='html') + # generate the response + return self.get_response(res) + + def post(self, request): + """ + return response to a POST request + """ + + if not request.path_info.strip('/'): + raise exc.HTTPMethodNotAllowed() + + # get the file + f = self.file(request.path_info) + if not f: + raise exc.HTTPNotFound() + if os.path.isdir(f): + raise exc.HTTPMethodNotAllowed() + + regex = '\[([^\]]+)\](.*)' + + conf = ConfigMunger(f) + + delete = request.POST.getall('delete') + if delete: + del request.POST['delete'] + + for key, value in request.POST.items(): + value = ' '.join(value.strip().split()) + match = re.match(regex, key) + section, option = match.groups() + if option: + conf.set(section, option, value) + else: + if value: + conf.move_section(section, value) + else: + conf.add_section(section) + + for d in delete: + match = re.match(regex, d) + section, option = match.groups() + if conf.has_section(section): + if option: + conf.remove_option(section, option) + else: + conf.remove_section(section) + + output = file(f, 'w') + conf.write(output) + + return exc.HTTPSeeOther(location=request.path_info) + + def error(self, request): + """deal with non-supported methods""" + return exc.HTTPMethodNotAllowed("Only %r operations are allowed" % self.response_functions.keys()) + + +### paste factory + +def factory(global_conf, **app_conf): + """create a webob view and wrap it in middleware""" + keystr = 'martini.' + args = dict([(key.split(keystr, 1)[-1], value) + for key, value in app_conf.items() + if key.startswith(keystr) ]) + app = MartiniWebView(**args) + return HTTPExceptionHandler(app) diff -r 000000000000 -r 3c3522ce6e3a martiniweb.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/martiniweb.ini Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,23 @@ +#!/usr/bin/env paster + +[DEFAULT] +debug = true +email_to = +smtp_server = localhost +error_email_from = paste@localhost + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 1025 + +[composite:main] +use = egg:Paste#urlmap +/ = martINI + +set debug = false + +[app:martINI] +paste.app_factory = martini.web:factory +martini.files = %(here)s/example/example.ini +martini.directories = %(here)s/example/conf \ No newline at end of file diff -r 000000000000 -r 3c3522ce6e3a setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Tue Dec 08 15:13:28 2009 -0500 @@ -0,0 +1,37 @@ +from setuptools import setup, find_packages +import sys, os + +version = '0.2.3' + +setup(name='martINI', + version=version, + description="edit .ini files from the command line", + long_description="""\ +""", + classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers + keywords='ini cli', + author='Jeff Hammel', + author_email='jhammel@openplans.org', + url='http://k0s.org', + license='GPL', + packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), + include_package_data=True, + zip_safe=False, + install_requires=[ + # -*- Extra requirements: -*- + 'WebOb', + 'Paste', + 'PasteScript', + 'genshi' + ], + entry_points=""" + # -*- Entry points: -*- + [console_scripts] + ini-get = martini.main:get + ini-set = martini.main:set + ini-delete = martini.main:delete + + [paste.app_factory] + main = martini.web:factory + """, + )