> Hi Edwin, > > This new system looks very well thought out. I've got a few > suggestions but it should be landable quickly. > > Also note that the MP is targeted to db-devel, which messed up your diff. I > assume you do plan to land against devel, so be careful. > > --Brad Hi Brad, Thanks for the review. Sorry about the bad diff. > > > === modified file 'Makefile' > > --- Makefile 2010-01-29 16:03:04 +0000 > +++ Makefile 2010-02-09 17:18:07 +0000 > > @@ -18,7 +18,8 @@ > > LPCONFIG=development > > > > JSFLAGS= > > -LP_BUILT_JS_ROOT=lib/canonical/launchpad/icing/build > > +ICING=lib/canonical/launchpad/icing > > +LP_BUILT_JS_ROOT=${ICING}/build > > LAZR_BUILT_JS_ROOT=lazr-js/build > > > > MINS_TO_SHUTDOWN=15 > > @@ -111,9 +112,19 @@ > > > > build: $(BZR_VERSION_INFO) compile apidoc jsbuild css_combine > > > > -css_combine: > > +css_combine: sprite_css > > ${SHHH} bin/combine-css > > > > +sprite_css: ${LP_BUILT_JS_ROOT}/style-3-0.css > > + > > +${LP_BUILT_JS_ROOT}/style-3-0.css: ${ICING}/style-3-0.css.in ${ICING}/icon- > sprites.positioning > > + ${SHHH} bin/sprite-util create-css > > + > > +${ICING}/icon-sprites: > > + ${SHHH} bin/sprite-util create-image > > + > > +sprite_image: ${ICING}/icon-sprites > > This should have a dependency rule so it isn't run every time or it > should not be part of the 'make build' chain if it is just to be run > on demand. sprite_css is a dependency of css_combine, but sprite_image is not a dependency of anything, so it is only run manually when a new image needs to be added. > > + > > jsbuild_lazr: > > # We absolutely do not want to include the lazr.testing module and its > > # jsTestDriver test harness modifications in the lazr.js and > launchpad.js > > > === added file 'buildout-templates/bin/sprite-util.in' > > --- buildout-templates/bin/sprite-util.in 1970-01-01 00:00:00 +0000 > > +++ buildout-templates/bin/sprite-util.in 2010-02-09 16:59:33 +0000 > > @@ -0,0 +1,41 @@ > > +#! /usr/bin/env ${buildout:directory}/bin/py > > Why the space between ! and /? I know you're following the pattern > but it's ugly. Yeah, I just copied another file and never looked at that. Fixed. > > + > > +import os > > +import sys > > + > > +from lp.services.spriteutils import SpriteUtil > > + > > +command_options = ('create-image', 'create-css') > > + > > +if len(sys.argv) != 2: > > + print >> sys.stderr, "Expected a single argument." > > + print >> sys.stderr, " Usage: %s %s" % ( > > + sys.argv[0], '|'.join(command_options)) > > + sys.exit(1) > > +else: > > + command = sys.argv[1] > > + if command not in command_options: > > + print >> sys.stderr, "Unknown argument: %s" % command > > + print >> sys.stderr, " Usage: %s %s" % ( > > + sys.argv[0], '|'.join(command_options)) > > + sys.exit(2) > > Refactor a usage() method. Fixed. > > + > > +root = '${buildout:directory}' > > +icing = os.path.join(root, 'lib/canonical/launchpad/icing') > > +combined_image_file = os.path.join(icing, 'icon-sprites') > > +positioning_file = os.path.join(icing, 'icon-sprites.positioning') > > +css_template_file = os.path.join(icing, 'style-3-0.css.in') > > +css_file = os.path.join(icing, 'build/style-3-0.css') > > I'm concerned that the paths are here and in the Makefile. Could the > makefile pass it in? It's a little problematic having the Makefile pass the paths in. If sprite-util accepts paths as arguments, it makes it will continue to work after changing the argument, but there is a lot of additional information in sprite-util that would break if the paths were changed. For example, the background-image needs to have a relative url path from the css to the combined image. If you moved either of those files, it would have to be updated appropriately. Even if the relative url matched the relative filesystem path, you would have to pass the css template, the generated css filename and the generated image filename to get "sprite-util create-css" to work. That seems a little much. Alternatively, I believe I could add commands to sprite-util that would enable us to set Makefile variables with the proper paths. > > === renamed file 'lib/canonical/launchpad/icing/style-3-0.css' => > 'lib/canonical/launchpad/icing/style-3-0.css.in' > > --- lib/canonical/launchpad/icing/style-3-0.css 2010-02-05 09:48:47 > +0000 > > +++ lib/canonical/launchpad/icing/style-3-0.css.in 2010-02-09 16:59:33 > +0000 > > Were these changes done by hand or generated? I hope the latter b/c I > didn't look at them all carefully. The only parts that were done by hand instead of automating it were the removal of ".footer .lp-arcana .search-icon" and the addition of .icon-only. > > === modified file 'lib/lp/app/templates/base-layout-macros.pt' > > --- lib/lp/app/templates/base-layout-macros.pt 2010-01-30 18:15:36 > +0000 > > +++ lib/lp/app/templates/base-layout-macros.pt 2010-02-09 16:59:33 > +0000 > > @@ -338,10 +338,10 @@ > > Read the guide > >   > >
> - class="sprite-after search-icon" > > tal:condition="view/macro:pagehas/globalsearch" > > tal:attributes="action string:${rooturl}+search"> > > > > + > >
> > > > > > === added directory 'lib/lp/services/doc' > > === added file 'lib/lp/services/doc/sprites.txt' > > --- lib/lp/services/doc/sprites.txt 1970-01-01 00:00:00 +0000 > > +++ lib/lp/services/doc/sprites.txt 2010-02-09 16:59:33 +0000 > > @@ -0,0 +1,186 @@ > > +Sprites > > +======= > > + > > +Many small images in Launchpad are combined into a single image file, > > +so that browsers do not have to make as many requests when they first > > +view a page. An individual sprite is displayed as a background image > > +on an element by setting the background position. > > + > > +A new image can be added to the combined file with the command: > > + make sprite_image > > + > > +The resulting icon-sprites and icon-sprites.positioning files must > > +be committed to the repository. Any changes to the CSS template will > > +be automatically picked up when Launchpad is restarted. If you don't > > +want to restart launchpad.dev, you can run: > > + make css_combine > > + > > + > > +CSS Template > > +------------ > > + > > +SpriteUtil takes a template css file that contains special comments > > +indicating that the background-image for a given rule should be > > +added to the combined image file and the rule should be updated. > > + > > +For example: > > + .add { > > + background-image: url(/@@/edit.png); /* sprite-ref: group1 */ > > + } > > +would become > > + .add { > > + background-image: url(foo/combined_image.png); > > + background-position: 0px 234px; > > + } > > + > > +The sprite-ref parameter specifies not only that the given image > > +should become a sprite but also that all the images with sprite-ref > > +value (in this case: "group1") will be combined to a single file. > > +A SpriteUtil object currently can only process a single group. > > + > > +Loading the CSS Template > > +------------------------ > > + > > +Instatiating a new SpriteUtil object will parse the css template and > > +find all the css rules for the specified group. The > url_prefix_substitutions > > +parameter allows you to convert url prefixes into file paths relative > > +to the directory passed into combineImages(). The space between sprites > > +in the file can be changed with the margin parameter. > > + > > + >>> import os, tempfile > > + >>> import Image > > + >>> from lp.services.spriteutils import SpriteUtil > > + >>> root = os.path.abspath(os.path.join(__file__, '../../../../..')) > > + >>> icing = os.path.join(root, 'lib/canonical/launchpad/icing') > > + >>> new_png_file = tempfile.NamedTemporaryFile() > > + >>> new_positioning_file = tempfile.NamedTemporaryFile() > > + >>> new_css_file = tempfile.NamedTemporaryFile() > > + >>> def get_sprite_util(margin=0): > > + ... return SpriteUtil( > > + ... os.path.join( > > + ... root, 'lib/lp/services/tests/testfiles/template.css'), > > + ... 'group1', > > + ... url_prefix_substitutions={'/@@/': '../images/'}, > > + ... margin=margin) > > + >>> sprite_util = get_sprite_util() > > + > > + > > +Generate Image File > > +------------------- > > + > > +The combined image will have a width equal to that of the widest image, > > +and the height will be the sum of all the image heights, since the margin > > +is currently zero. > > + > > + >>> sprite_util.combineImages(icing) > > + >>> sprite_util.savePNG(new_png_file.name) > > + >>> image = Image.open(new_png_file.name) > > + >>> print image.size > > + (32, 87) > > + > > +The height will increase when the margin is increased. > > + > > + >>> sprite_util = get_sprite_util(margin=100) > > + >>> sprite_util.combineImages(icing) > > + >>> sprite_util.savePNG(new_png_file.name) > > + >>> image = Image.open(new_png_file.name) > > + >>> print image.size > > + (32, 587) > > + > > + > > +Positioning File > > +---------------- > > + > > +The positioning file contains locations of each sprite in the combined > > +image file. This allows the css file to be regenerated when the template > > +changes without requiring the combined image file to be recreated. > > + > > + >>> sprite_util.savePositioning(new_positioning_file.name) > > + >>> print new_positioning_file.read() > > + /*... > > + { > > + "../images/edit.png": [ > > + 0, > > + -228 > > + ], > > + "../images/flame-large.png": [ > > + 0, > > + -456 > > + ], > > + "../images/blue-bar.png": [ > > + 0, > > + -342 > > + ], > > + "../images/add.png": [ > > + 0, > > + -114 > > + ] > > + } > > +The positions attribute can be cleared and loaded from the file. > > + > > + >>> from pprint import pprint > > + >>> pprint(sprite_util.positions) > > + {u'../images/add.png': (0, -114), > > + u'../images/blue-bar.png': (0, -342), > > + u'../images/edit.png': (0, -228), > > + u'../images/flame-large.png': (0, -456)} > > Does pprint guarantee sorting? Hmm, it looks like it does. I never > knew that. I looked into it. pprint only sorts dictionaries if the repr is too long for it to fit on one line. There is a test global pretty() that sets up a pretty printer with width=1, so that it always gets sorted. I'm using that now. > > + >>> sprite_util.positions = None > > + >>> sprite_util.loadPositioning(new_positioning_file.name) > > + >>> pprint(sprite_util.positions) > > + {'../images/add.png': [0, -114], > > + '../images/blue-bar.png': [0, -342], > > + '../images/edit.png': [0, -228], > > + '../images/flame-large.png': [0, -456]} > > + > > + > > +Generate CSS File > > +----------------- > > + > > +When the css file is generated, the second parameter is the relative > > +path from the css file to the combined image file. The .add and .foo > > +classes have the same background-position, since they both originally > > +referenced /@@/add.png, which was only added once to the combined file. > > +.bar and .info do not have a background-position and the background-image > > +is not group1.png, since its sprite-ref is "group2". > > + > > + >>> sprite_util.saveConvertedCSS(new_css_file.name, 'group1.png') > > + >>> print new_css_file.read() > > + /*... > > + .add { > > + background-image: url(group1.png); > > + /* sprite-ref: group1 */ > > + background-position: 0 -114px > > + } > > + .foo { > > + background-image: url(group1.png); > > + /* sprite-ref: group1 */ > > + background-position: 0 -114px > > + } > > + .bar { > > + background-image: url(/@@/add.png); > > + /* sprite-ref: group2 */ > > + } > > + .edit { > > + background-image: url(group1.png); > > + /* sprite-ref: group1 */ > > + background-repeat: no-repeat; > > + background-position: 0 -228px > > + } > > + .info { > > + background-image: url(/@@/info.png); > > + /* sprite-ref: group2 */ > > + background-repeat: no-repeat > > + } > > + .bluebar { > > + background-image: url(group1.png); > > + /* sprite-ref: group1 */ > > + background-repeat: repeat-x; > > + background-position: 0 -342px > > + } > > + .large-flame { > > + background-image: url(group1.png); > > + /* sprite-ref: group1 */ > > + background-repeat: no-repeat; > > + background-position: 0 -456px > > + } > > > === added file 'lib/lp/services/spriteutils.py' > > --- lib/lp/services/spriteutils.py 1970-01-01 00:00:00 +0000 > > +++ lib/lp/services/spriteutils.py 2010-02-09 16:59:33 +0000 > > @@ -0,0 +1,243 @@ > > +# Copyright 2010 Canonical Ltd. This software is licensed under the > > +# GNU Affero General Public License version 3 (see the file LICENSE). > > +# > > +# Derived from make_master.py by Oran Looney. > > +# http://oranlooney.com/make-css-sprites-python-image-library/ > > + > > +"""Library to create sprites.""" > > + > > +from __future__ import with_statement > > + > > +__metaclass__ = type > > + > > +__all__ = [ > > + 'SpriteUtil', > > + ] > > + > > +import os > > +import sys > > +import re > > +import cssutils > > +import Image > > +import simplejson > > +from textwrap import dedent > > + > > Another blank line is needed here. Fixed. > > +class SpriteUtil: > > + EDIT_WARNING = dedent("""\ > > + /* DO NOT EDIT THIS FILE BY HAND!!! */ > > Is bang-bang-bang necessary? :) I personally ignore anything that isn't extra emphatic. I don't really care, and it didn't sound like you care, so I'll just leave it, and you can let me know!!! > > + /* It is autogenerated by spriteutils. */ > > + """) > > + > > + def __init__(self, css_template_file, group_name, > > + url_prefix_substitutions=None, margin=150): > > + """Initialize with the specified css template file. > > + > > + :param css_template_file: (str) Name of the file containing > > + > > Extra blank line Fixed. > > + css rules with a background-image style that needs to be > > + combined into the sprite file, a comment allowing sprites to > > + be grouped into different image files, and a > > + background-repeat style if necessary. Currently, "repeat-y" > > + is not supported since the file is combined vertically, so > > + repeat-y would show the entire combined image file. > > + > > + Example css template: > > + edit-icon { > > + background-image: url(../edit.png) > > + /* sprite-ref: group1 */ > > + } > > + blue-bar { > > + background-image: url(../blue-bar.png) > > + /* sprite-ref: group1 */ > > + background-repeat: repeat-x > > + } > > + > > + :param group_name: (str) Only add sprites to the > > + combined image file whose sprite-ref comment in the > > + css template match this group-name. > > + > > + :param url_prefix_substitutions: (dict) The css template > > + will contain references to image files by their url > > + path, but the filesystem path relative to the css > > + template is needed. > > + > > + :param margin: (int) The number of pixels between each sprite. > > + Be aware that Firefox will ignore extremely large images, > > + for example 64x34000 pixels. > > + > > + If the css_template_file has been modified, a new > > + css file using an existing combined image and positioning > > + file can be generated using: > > + sprite_util = SpriteUtil(...) > > + sprite_util.loadPositioning(...) > > + sprite_util.saveConvertedCSS(...) > > + > > + If a new image file needs to be added to the combined image > > + and the positioning file, they can be regenerated with: > > + sprite_util = SpriteUtil(...) > > + sprite_util.combineImages(...) > > + sprite_util.savePNG(...) > > + sprite_util.savePositioning(...) > > + > > + If the image file is regenerated any time the css file is > > + regenerated, then the step for saving and loading the positioning > > + information could be removed. For example: > > + sprite_util = SpriteUtil(...) > > + sprite_util.combineImages(...) > > + sprite_util.savePNG(...) > > + sprite_util.saveConvertedCSS(...) > > + """ > > + self.combined_image = None > > + self.positions = None > > + self.group_name = group_name > > + self.margin = margin > > + self._loadCSSTemplate( > > + css_template_file, group_name, url_prefix_substitutions) > > + > > + def _loadCSSTemplate(self, css_template_file, group_name, > > + url_prefix_substitutions=None): > > + """See `__init__`.""" > > + smartsprites_exp = re.compile( > > + r'/\*+([^*]*sprite-ref: [^*]*)\*/') > > + self.css_object = cssutils.parseFile(css_template_file) > > + self.sprite_info = [] > > + for rule in self.css_object: > > + if rule.cssText is None: > > + continue > > + match = smartsprites_exp.search(rule.cssText) > > + if match is not None: > > + smartsprites_info = match.group(1) > > + parameters = self._parseCommentParameters(match.group(1)) > > + # Currently, only a single combined image is supported. > > + if parameters['sprite-ref'] == group_name: > > + filename = self._getSpriteImagePath( > > + rule, url_prefix_substitutions) > > + self.sprite_info.append( > > + dict(filename=filename, rule=rule)) > > + > > + if len(self.sprite_info) == 0: > > + raise AssertionError( > > + "No sprite-ref comments for group %r found" % group_name) > > + > > + def _getSpriteImagePath(self, rule, url_prefix_substitutions=None): > > + """Convert the url path to a filesystem path.""" > > + # Remove url() from string. > > + filename = rule.style.backgroundImage[4:-1] > > + # Convert urls to paths relative to the css > > + # file, e.g. '/@@/foo.png' => '../images/foo.png'. > > + if url_prefix_substitutions is not None: > > + for old, new in url_prefix_substitutions.items(): > > + if filename.startswith(old): > > + filename = new + filename[len(old):] > > + return filename > > + > > + def _parseCommentParameters(self, parameter_string): > > + """Parse parameters out of javascript comments. > > + > > + Currently only used for the group name specified > > + by "sprite-ref". > > + """ > > + results = {} > > + for parameter in parameter_string.split(';'): > > + if parameter.strip() != '': > > + name, value = parameter.split(':') > > + name = name.strip() > > + value = value.strip() > > + results[name] = value > > + return results > > + > > + def combineImages(self, css_dir): > > + """Copy all the sprites into a single PIL image.""" > > + > > + # Open all the sprite images. > > + sprite_images = {} > > + max_sprite_width = 0 > > + total_sprite_height = 0 > > + for sprite in self.sprite_info: > > + abs_filename = os.path.join(css_dir, sprite['filename']) > > + sprite_images[sprite['filename']] = Image.open(abs_filename) > > + width, height = sprite_images[sprite['filename']].size > > + max_sprite_width = max(width, max_sprite_width) > > + total_sprite_height += height > > + > > + # The combined image is the height of all the sprites > > + # plus the margin between each of them. > > + combined_image_height = ( > > + total_sprite_height + (self.margin * len(self.sprite_info) - > 1)) > > + transparent = (0, 0, 0, 0) > > + combined_image = Image.new( > > + mode='RGBA', > > + size=(max_sprite_width, combined_image_height), > > + color=transparent) > > + > > + # Paste each sprite into the combined image. > > + y = 0 > > + positions = {} > > + for index, sprite in enumerate(self.sprite_info): > > + sprite_image = sprite_images[sprite['filename']] > > + try: > > + position = [0, y] > > + combined_image.paste(sprite_image, tuple(position)) > > + # An icon in a vertically combined image can be repeated > > + # horizontally, but we have to repeat it in the combined > > + # image so that we don't repeat white space. > > + if sprite['rule'].style.backgroundRepeat == 'repeat-x': > > + width = sprite_image.size[0] > > + for x_position in range(width, max_sprite_width, > width): > > + position[0] = x_position > > + combined_image.paste(sprite_image, tuple(position)) > > + except: > > + print >> sys.stderr, ( > > + "Error with image file %s" % sprite['filename']) > > + raise > > + # This is the position of the combined image on an HTML > > + # element. Therefore, it subtracts the position of the > > + # sprite in the file to move it to the top of the element. > > + positions[sprite['filename']] = (0, -y) > > + y += sprite_image.size[1] + self.margin > > + > > + # If there is an exception earlier, these attributes will remain > None. > > + self.positions = positions > > + self.combined_image = combined_image > > + > > + def savePNG(self, filename): > > + """Save the PIL image object to disk.""" > > + self.combined_image.save(filename, format='png', optimize=True) > > + > > + def savePositioning(self, filename): > > + """Save the positions of sprites in the combined image. > > + > > + This allows the final css to be generated after making > > + changes to the css template without recreating the combined > > + image file. > > + """ > > + fp = open(filename, 'w') > > + fp.write(self.EDIT_WARNING) > > + simplejson.dump(self.positions, fp=fp, indent=4) > > + > > + def loadPositioning(self, filename): > > + """Load file with the positions of sprites in the combined > image.""" > > + json = open(filename).read() > > + # Remove comments from the beginning of the file. > > + start = json.index('{') > > + json = json[start:] > > + self.positions = simplejson.loads(json) > > + > > + def saveConvertedCSS(self, css_file, combined_image_url_path): > > + """Generate new css from the template and the positioning info. > > + > > + Example css template: > > + background-image: url(../edit.png); /* sprite-ref: group1 */ > > + Example css output: > > + background-image: url(combined_image_url_path) > > + background-position: 0px 2344px > > + """ > > + for sprite in self.sprite_info: > > + rule = sprite['rule'] > > + rule.style.backgroundImage = 'url(%s)' % > combined_image_url_path > > + position = self.positions[sprite['filename']] > > + rule.style.backgroundPosition = '%dpx %dpx' % tuple(position) > > + > > + with open(css_file, 'w') as fp: > > + fp.write(self.EDIT_WARNING) > > + fp.write(self.css_object.cssText) > > > === added file 'lib/lp/services/tests/test_doc.py' > > --- lib/lp/services/tests/test_doc.py 1970-01-01 00:00:00 +0000 > > +++ lib/lp/services/tests/test_doc.py 2010-02-09 16:59:33 +0000 > > @@ -0,0 +1,17 @@ > > +# Copyright 2010 Canonical Ltd. This software is licensed under the > > +# GNU Affero General Public License version 3 (see the file LICENSE). > > + > > +""" > > +Run the doctests and pagetests. > > +""" > > + > > +import os > > + > > +from lp.services.testing import build_test_suite > > + > > + > > +here = os.path.dirname(os.path.realpath(__file__)) > > + > > + > > +def test_suite(): > > + return build_test_suite(here) > > > === added directory 'lib/lp/services/tests/testfiles' > > === added file 'lib/lp/services/tests/testfiles/template.css' > > --- lib/lp/services/tests/testfiles/template.css 1970-01-01 00:00:00 > +0000 > > +++ lib/lp/services/tests/testfiles/template.css 2010-02-09 16:59:33 > +0000 > > @@ -0,0 +1,20 @@ > > +.add { background-image: url(/@@/add.png); /* sprite-ref: group1 */ } > > +.foo { background-image: url(/@@/add.png); /* sprite-ref: group1 */ } > > +.bar { background-image: url(/@@/add.png); /* sprite-ref: group2 */ } > > +.edit { > > + background-image: url(/@@/edit.png); > > + /* sprite-ref: group1 */ > > + background-repeat: no-repeat; > > +} > > +.info { > > + background-image: url(/@@/info.png); /* sprite-ref: group2 */ > > + background-repeat: no-repeat; > > +} > > +.bluebar { > > + background-image: url(/@@/blue-bar.png); /* sprite-ref: group1 */ > > + background-repeat: repeat-x; > > +} > > +.large-flame { > > + background-image: url(/@@/flame-large.png); /* sprite-ref: group1 */ > > + background-repeat: no-repeat; > > +} Incremental diff: {{{ === modified file 'buildout-templates/bin/combine-css.in' --- buildout-templates/bin/combine-css.in 2010-02-02 23:10:50 +0000 +++ buildout-templates/bin/combine-css.in 2010-02-09 22:03:28 +0000 @@ -1,4 +1,4 @@ -#! /usr/bin/env ${buildout:directory}/bin/py +#!/usr/bin/env ${buildout:directory}/bin/py import os === modified file 'buildout-templates/bin/sprite-util.in' --- buildout-templates/bin/sprite-util.in 2010-02-09 03:16:27 +0000 +++ buildout-templates/bin/sprite-util.in 2010-02-10 05:59:55 +0000 @@ -1,4 +1,4 @@ -#! /usr/bin/env ${buildout:directory}/bin/py +#!/usr/bin/env ${buildout:directory}/bin/py import os import sys @@ -7,17 +7,18 @@ command_options = ('create-image', 'create-css') +def usage(): + return " Usage: %s %s" % (sys.argv[0], '|'.join(command_options)) + if len(sys.argv) != 2: print >> sys.stderr, "Expected a single argument." - print >> sys.stderr, " Usage: %s %s" % ( - sys.argv[0], '|'.join(command_options)) + print >> sys.stderr, usage() sys.exit(1) else: command = sys.argv[1] if command not in command_options: print >> sys.stderr, "Unknown argument: %s" % command - print >> sys.stderr, " Usage: %s %s" % ( - sys.argv[0], '|'.join(command_options)) + print >> sys.stderr, usage() sys.exit(2) root = '${buildout:directory}' @@ -37,5 +38,6 @@ sprite_util.savePositioning(positioning_file) elif command == 'create-css': sprite_util.loadPositioning(positioning_file) - # icon-sprites file is relative to the icing directory. + # The icing/icon-sprites file is relative to the css file + # in the icing/build/ directory. sprite_util.saveConvertedCSS(css_file, '../icon-sprites') === modified file 'lib/lp/services/doc/sprites.txt' --- lib/lp/services/doc/sprites.txt 2010-02-09 03:16:27 +0000 +++ lib/lp/services/doc/sprites.txt 2010-02-09 21:55:12 +0000 @@ -119,15 +119,14 @@ The positions attribute can be cleared and loaded from the file. - >>> from pprint import pprint - >>> pprint(sprite_util.positions) + >>> pretty(sprite_util.positions) {u'../images/add.png': (0, -114), u'../images/blue-bar.png': (0, -342), u'../images/edit.png': (0, -228), u'../images/flame-large.png': (0, -456)} >>> sprite_util.positions = None >>> sprite_util.loadPositioning(new_positioning_file.name) - >>> pprint(sprite_util.positions) + >>> pretty(sprite_util.positions) {'../images/add.png': [0, -114], '../images/blue-bar.png': [0, -342], '../images/edit.png': [0, -228], === modified file 'lib/lp/services/spriteutils.py' --- lib/lp/services/spriteutils.py 2010-02-09 04:36:21 +0000 +++ lib/lp/services/spriteutils.py 2010-02-09 22:02:11 +0000 @@ -22,6 +22,7 @@ import simplejson from textwrap import dedent + class SpriteUtil: EDIT_WARNING = dedent("""\ /* DO NOT EDIT THIS FILE BY HAND!!! */ @@ -33,7 +34,6 @@ """Initialize with the specified css template file. :param css_template_file: (str) Name of the file containing - css rules with a background-image style that needs to be combined into the sprite file, a comment allowing sprites to be grouped into different image files, and a }}}