diff options
| author | Andi Albrecht <albrecht.andi@gmail.com> | 2011-08-09 16:04:40 +0200 |
|---|---|---|
| committer | Andi Albrecht <albrecht.andi@gmail.com> | 2011-08-09 16:04:40 +0200 |
| commit | 5d015084e5bf640f41c8d93c89d2ff70c0967c2b (patch) | |
| tree | cfa3b58e11e49a6266a2a05ab9e477c5d62c5024 | |
| parent | a8b46d3b96ead22263de782b36090f2f1bc7c15d (diff) | |
| parent | 530da46905753a1e950bcc7e736d72a7309033c6 (diff) | |
| download | sqlparse-5d015084e5bf640f41c8d93c89d2ff70c0967c2b.tar.gz | |
merge
40 files changed, 400 insertions, 673 deletions
@@ -5,7 +5,7 @@ dist MANIFEST .coverage extras/appengine/sqlparse -extras/appengine/pygments +extras/appengine/lib/ extras/py3k/sqlparse extras/py3k/tests extras/py3k/sqlparse.diff @@ -1,3 +1,4 @@ 49f461d2899eb3175ba7d935337e5938403372d4 0.1.0 294617d5c43131aacd11ddccc9ccb238a41bf563 0.1.1 079b282ad3ee0d620128ee64e2bba5791e733b62 0.1.2 +ac9ea581527a4540d46d766c07b23a88dd04704c 0.1.3 @@ -1,5 +1,5 @@ -In Development --------------- +Release 0.1.3 (Jul 29, 2011) +---------------------------- Bug Fixes * Improve parsing of floats (thanks to Kris). @@ -16,6 +16,11 @@ Bug Fixes * Improve parsing of stand-alone comments (issue26). * Detection of placeholders in paramterized queries (issue22, reported by Glyph Lefkowitz). + * Add parsing of MS Access column names with braces (issue27, + reported by frankz...@gmail.com). + +Other + * Replace Django by Flask in App Engine frontend (issue11). Release 0.1.2 (Nov 23, 2010) diff --git a/extras/appengine/Makefile b/extras/appengine/Makefile deleted file mode 100644 index 31393c1..0000000 --- a/extras/appengine/Makefile +++ /dev/null @@ -1,45 +0,0 @@ -# Makefile to simplify some common AppEngine actions. -# Use 'make help' for a list of commands. - -PYTHON=`which python2.5` -DEV_APPSERVER=$(PYTHON) `which dev_appserver.py` -APPCFG=$(PYTHON) `which appcfg.py` -PORT=8080 - - -default: help - -help: - @echo "Available commands:" - @sed -n '/^[a-zA-Z0-9_.]*:/s/:.*//p' <Makefile | sort - -serve: - $(DEV_APPSERVER) --port=$(PORT) . - -serve_remote: - $(DEV_APPSERVER) --port=$(PORT) --address 0.0.0.0 . - -serve_email: - $(DEV_APPSERVER) --port=$(PORT) --enable_sendmail . - -serve_remote_email: - $(DEV_APPSERVER) --port=$(PORT) --enable_sendmail --address 0.0.0.0 . - -update: - $(APPCFG) update . - -upload: update - -update_indexes: - $(APPCFG) update_indexes . - -vacuum_indexes: - $(APPCFG) vacuum_indexes . - -all: pygments sqlparse - -pygments: - ln -s `python -c "import pygments,os; print os.path.dirname(pygments.__file__)"` . - -sqlparse: - ln -s ../../sqlparse . diff --git a/extras/appengine/README b/extras/appengine/README index 4762faa..04d32b2 100644 --- a/extras/appengine/README +++ b/extras/appengine/README @@ -1,22 +1,3 @@ -gae-sqlformat - An SQL formatting tool runnging on App Engine -============================================================= - - -To set up this application run - - make all - -This command fetches Django from Subversion and symlinks Pygments -and sqlparse. Note: You'll need Pygments installed somewhere in your -PYTHONPATH. - -For a manual setup have a look at the Makefile ;-) - - -To run the development server run - - make serve - - -Homepage: http://sqlformat.appspot.com - +- Run "./bootstrap.py" to download all required Python modules. +- Run "dev_appserver.py ." for a local server. +- Have a look at config.py :) diff --git a/extras/appengine/__init__.py b/extras/appengine/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/extras/appengine/__init__.py +++ /dev/null diff --git a/extras/appengine/app.yaml b/extras/appengine/app.yaml index 14aab5e..3afa891 100644 --- a/extras/appengine/app.yaml +++ b/extras/appengine/app.yaml @@ -1,5 +1,5 @@ application: sqlformat -version: 2 +version: dev runtime: python api_version: 1 @@ -23,8 +23,15 @@ handlers: - url: /static static_dir: static +- url: /_ereporter.* + script: $PYTHON_LIB/google/appengine/ext/ereporter/report_generator.py + login: admin + - url: .* script: main.py -derived_file_type: -- python_precompiled
\ No newline at end of file +builtins: +- appstats: on + +inbound_services: +- warmup diff --git a/extras/appengine/appengine_config.py b/extras/appengine/appengine_config.py new file mode 100644 index 0000000..8ed3ee5 --- /dev/null +++ b/extras/appengine/appengine_config.py @@ -0,0 +1,6 @@ + +def webapp_add_wsgi_middleware(app): + from google.appengine.ext.appstats import recording + app = recording.appstats_wsgi_middleware(app) + return app + diff --git a/extras/appengine/bootstrap.py b/extras/appengine/bootstrap.py new file mode 100755 index 0000000..debc2bf --- /dev/null +++ b/extras/appengine/bootstrap.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python + +"""Downloads required third-party modules.""" + +import os +import urllib2 +import gzip +import tarfile +import tempfile +import shutil +import sys +from StringIO import StringIO + +HERE = os.path.abspath(os.path.dirname(__file__)) +LIB_DIR = os.path.join(HERE, 'lib') + +PACKAGES = { + 'http://pypi.python.org/packages/source/F/Flask/Flask-0.7.2.tar.gz': + [('Flask-0.7.2/flask', 'flask')], + 'http://pypi.python.org/packages/source/W/Werkzeug/Werkzeug-0.6.2.tar.gz': + [('Werkzeug-0.6.2/werkzeug', 'werkzeug')], + 'http://pypi.python.org/packages/source/J/Jinja2/Jinja2-2.5.5.tar.gz': + [('Jinja2-2.5.5/jinja2/', 'jinja2')], + 'http://pypi.python.org/packages/source/s/simplejson/simplejson-2.1.6.tar.gz': + [('simplejson-2.1.6/simplejson', 'simplejson')], + 'http://pypi.python.org/packages/source/P/Pygments/Pygments-1.4.tar.gz': + [('Pygments-1.4/pygments', 'pygments')], +} + + +def fetch_all(): + if not os.path.isdir(LIB_DIR): + os.makedirs(LIB_DIR) + for url, targets in PACKAGES.iteritems(): + if not _missing_targets(targets): + continue + sys.stdout.write(url) + sys.stdout.flush() + fetch(url, targets) + sys.stdout.write(' done\n') + sys.stdout.flush() + + +def fetch(url, targets): + blob = urllib2.urlopen(url).read() + gz = gzip.GzipFile(fileobj=StringIO(blob)) + tar = tarfile.TarFile(fileobj=gz) + tmpdir = tempfile.mkdtemp() + try: + tar.extractall(tmpdir) + for src, dest in targets: + dest = os.path.join(LIB_DIR, dest) + if os.path.isdir(dest): + shutil.rmtree(dest) + shutil.copytree(os.path.join(tmpdir, src), dest) + finally: + shutil.rmtree(tmpdir) + + +def _missing_targets(targets): + for _, dest in targets: + dest = os.path.join(LIB_DIR, dest) + if not os.path.isdir(dest): + return True + return False + + +def link_sqlparse(): + if os.path.islink('sqlparse'): + return + elif os.path.exists('sqlparse'): + shutil.rmtree('sqlparse') + if hasattr(os, 'symlink'): + os.symlink('../../sqlparse', 'sqlparse') + else: + shutil.copytree(os.path.join(HERE, '../../sqlparse'), + 'sqlparse') + + +if __name__ == '__main__': + fetch_all() + link_sqlparse() diff --git a/extras/appengine/config.py b/extras/appengine/config.py new file mode 100644 index 0000000..1599c00 --- /dev/null +++ b/extras/appengine/config.py @@ -0,0 +1,7 @@ +# SQLFormat configuration + +# Debug flag +DEBUG = True + +# Secret key, please change this +SECRET_KEY = 'notsosecret' diff --git a/extras/appengine/cron.yaml b/extras/appengine/cron.yaml new file mode 100644 index 0000000..a7fefce --- /dev/null +++ b/extras/appengine/cron.yaml @@ -0,0 +1,4 @@ +cron: +- description: Daily exception report + url: /_ereporter?sender=albrecht.andi@googlemail.com&versions=all&delete=false + schedule: every day 00:00
\ No newline at end of file diff --git a/extras/appengine/index.yaml b/extras/appengine/index.yaml index e69de29..7071349 100644 --- a/extras/appengine/index.yaml +++ b/extras/appengine/index.yaml @@ -0,0 +1,7 @@ +indexes: +- kind: __google_ExceptionRecord + properties: + - name: date + - name: major_version + - name: minor_version + direction: desc diff --git a/extras/appengine/main.py b/extras/appengine/main.py index 5014cb9..4cabba8 100644 --- a/extras/appengine/main.py +++ b/extras/appengine/main.py @@ -1,140 +1,41 @@ -# Copyright 2008 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Main program for Rietveld. - -This is also a template for running a Django app under Google App -Engine, especially when using a newer version of Django than provided -in the App Engine standard library. - -The site-specific code is all in other files: urls.py, models.py, -views.py, settings.py. -""" - -# Standard Python imports. +# SQLFormat's main script, dead simple :) + import os import sys + +from google.appengine.ext.webapp.util import run_wsgi_app + +LIB_DIR = os.path.join(os.path.dirname(__file__), 'lib') + +if LIB_DIR not in sys.path: + sys.path.insert(0, LIB_DIR) + +from sqlformat import app + +import config + import logging -import traceback - - -# Log a message each time this module get loaded. -logging.info('Loading %s, app version = %s', - __name__, os.getenv('CURRENT_VERSION_ID')) - -os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' - -from google.appengine.dist import use_library -use_library('django', '1.2') - -# Fail early if we can't import Django. Log identifying information. -import django -logging.info('django.__file__ = %r, django.VERSION = %r', - django.__file__, django.VERSION) -assert django.VERSION[0] >= 1, "This Django version is too old" - -# AppEngine imports. -from google.appengine.ext.webapp import util -from google.appengine.api import mail - - -# Helper to enter the debugger. This passes in __stdin__ and -# __stdout__, because stdin and stdout are connected to the request -# and response streams. You must import this from __main__ to use it. -# (I tried to make it universally available via __builtin__, but that -# doesn't seem to work for some reason.) -def BREAKPOINT(): - import pdb - p = pdb.Pdb(None, sys.__stdin__, sys.__stdout__) - p.set_trace() - - -# Custom Django configuration. -from django.conf import settings -settings._target = None - -# Import various parts of Django. -import django.core.handlers.wsgi -import django.core.signals -import django.db -import django.dispatch.dispatcher -import django.forms - -# Work-around to avoid warning about django.newforms in djangoforms. -django.newforms = django.forms - - -def log_exception(*args, **kwds): - """Django signal handler to log an exception.""" - excinfo = sys.exc_info() - cls, err = excinfo[:2] - subject = 'Exception in request: %s: %s' % (cls.__name__, err) - logging.exception(subject) - try: - repr_request = repr(kwds.get('request', 'Request not available.')) - except: - repr_request = 'Request repr() not available.' - msg = ('Application: %s\nVersion: %s\n\n%s\n\n%s' - % (os.getenv('APPLICATION_ID'), os.getenv('CURRENT_VERSION_ID'), - '\n'.join(traceback.format_exception(*excinfo)), - repr_request)) - mail.send_mail_to_admins('albrecht.andi@googlemail.com', - '[%s] %s' % (os.getenv('APPLICATION_ID'), subject), - msg) - - -# Log all exceptions detected by Django. -django.core.signals.got_request_exception.connect(log_exception) - -# Unregister Django's default rollback event handler. -#django.core.signals.got_request_exception.disconnect( -# django.db._rollback_on_exception) - - -def real_main(): - """Main program.""" - # Create a Django application for WSGI. - application = django.core.handlers.wsgi.WSGIHandler() - # Run the WSGI CGI handler with that application. - util.run_wsgi_app(application) - - -def profile_main(): - """Main program for profiling.""" - import cProfile - import pstats - import StringIO - - prof = cProfile.Profile() - prof = prof.runctx('real_main()', globals(), locals()) - stream = StringIO.StringIO() - stats = pstats.Stats(prof, stream=stream) - # stats.strip_dirs() # Don't; too many modules are named __init__.py. - stats.sort_stats('time') # 'time', 'cumulative' or 'calls' - stats.print_stats() # Optional arg: how many to print - # The rest is optional. - # stats.print_callees() - # stats.print_callers() - print '\n<hr>' - print '<h1>Profile</h1>' - print '<pre>' - print stream.getvalue()[:1000000] - print '</pre>' - -# Set this to profile_main to enable profiling. -main = real_main - - -if __name__ == '__main__': - main() +from google.appengine.ext import ereporter + +ereporter.register_logger() + + +class EreporterMiddleware(object): + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + try: + return self.app(environ, start_response) + except: + logging.exception('Exception in request:') + logging.debug(environ) + raise + + +app.config.from_object(config) + +app = EreporterMiddleware(app) + +run_wsgi_app(app) diff --git a/extras/appengine/settings.py b/extras/appengine/settings.py deleted file mode 100644 index 74af42c..0000000 --- a/extras/appengine/settings.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2008 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Minimal Django settings.""" - -import os - -APPEND_SLASH = False -DEBUG = os.environ['SERVER_SOFTWARE'].startswith('Dev') -DEBUG=False -INSTALLED_APPS = ( - 'sqlformat', -) -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.middleware.http.ConditionalGetMiddleware', -# 'codereview.middleware.AddUserToRequestMiddleware', -) -ROOT_URLCONF = 'sqlformat.urls' -TEMPLATE_CONTEXT_PROCESSORS = () -TEMPLATE_DEBUG = DEBUG -TEMPLATE_DIRS = ( - os.path.join(os.path.dirname(__file__), 'templates'), - ) -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - ) diff --git a/extras/appengine/sqlformat/__init__.py b/extras/appengine/sqlformat/__init__.py index e69de29..db9c132 100644 --- a/extras/appengine/sqlformat/__init__.py +++ b/extras/appengine/sqlformat/__init__.py @@ -0,0 +1,24 @@ +from flask import Flask, make_response + +from sqlformat.legacy import legacy + + +app = Flask('sqlformat') + + +@app.route('/ping') +def ping(): + return make_response('pong') + +@app.route('/_ah/warmup') +def warmup(): + return make_response('polishing chrome') + +@app.route('/fail') +def fail(): + # test URL for failure handling + raise AssertionError('You shouldn\'t be here!') + + +# Register legacy URLs last so that newer URLs replace them. +app.register_blueprint(legacy) diff --git a/extras/appengine/sqlformat/legacy.py b/extras/appengine/sqlformat/legacy.py new file mode 100644 index 0000000..6d3aaf8 --- /dev/null +++ b/extras/appengine/sqlformat/legacy.py @@ -0,0 +1,158 @@ +"""Legacy URLs.""" + +import logging +import os +import time + +from google.appengine.api import memcache + +from flask import Blueprint, make_response, render_template, Response, request + +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import SqlLexer, PythonLexer, PhpLexer + +import simplejson as json + +import sqlparse + + +legacy = Blueprint('', 'legacy') + + +EXAMPLES_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../examples')) + + +@legacy.route('/', methods=['POST', 'GET']) +def index(): + data = {'examples': _get_examples()} + extra = {'highlight': True, 'comments': False, + 'keywords': 'upper', 'idcase': '', + 'n_indents': '2', + 'lang': 'sql'} + sql_orig = 'select * from foo join bar on val1 = val2 where id = 123;' + if request.method == 'POST': + oformat = request.form.get('format', 'html') + extra['highlight'] = 'highlight' in request.form + extra['comments'] = 'remove_comments' in request.form + extra['keywords'] = request.form.get('keyword_case', '') + extra['idcase'] = request.form.get('identifier_case', '') + extra['n_indents'] = request.form.get('n_indents', '2') + extra['lang'] = request.form.get('output_format', 'sql') + sql = _get_sql(request.form, request.files) + sql_orig = sql + start = time.time() + data['output'] = _format_sql(sql, request.form, format=oformat) + data['proc_time'] = '%.3f' % (time.time()-start) + if oformat == 'json': + data['errors'] = '' + return make_response(Response(json.dumps(data), + content_type='text/x-json')) + elif oformat == 'text': + return make_response(Response(data['output'], content_type='text/plain')) + data['sql_orig'] = sql_orig + data['extra'] = extra + return render_template('index.html', **data) + + +@legacy.route('/source/') +def source(): + return render_template('source.html') + + +@legacy.route('/about/') +def about(): + return render_template('about.html') + +@legacy.route('/api/') +def api(): + return render_template('api.html') + + +@legacy.route('/format/', methods=['GET', 'POST']) +def format_(): + if request.method == 'POST': + sql = _get_sql(request.form, request.files) + data = request.form + else: + sql = _get_sql(request.args) + data = request.args + formatted = _format_sql(sql, data, format='text') + return make_response(Response(formatted, content_type='text/plain')) + + +@legacy.route('/load_example', methods=['GET', 'POST']) +def load_example(): + fname = request.form.get('fname') + if fname is None: + answer = 'Uups, I\'ve got no filename...' + elif fname not in _get_examples(): + answer = 'Hmm, I think you don\'t want to do that.' + else: + answer = open(os.path.join(EXAMPLES_DIR, fname)).read() + data = json.dumps({'answer': answer}) + return make_response(Response(data, content_type='text/x-json')) + + +def _get_examples(): + examples = memcache.get('legacy_examples') + if examples is None: + examples = os.listdir(EXAMPLES_DIR) + memcache.set('legacy_examples', examples) + return examples + + +def _get_sql(data, files=None): + sql = None + if files is not None and 'datafile' in files: + raw = files['datafile'].read() + try: + sql = raw.decode('utf-8') + except UnicodeDecodeError, err: + logging.error(err) + logging.debug(repr(raw)) + sql = (u'-- UnicodeDecodeError: %s\n' + u'-- Please make sure to upload UTF-8 encoded data for now.\n' + u'-- If you want to help improving this part of the application\n' + u'-- please file a bug with some demo data at:\n' + u'-- http://code.google.com/p/python-sqlparse/issues/entry\n' + u'-- Thanks!\n' % err) + if not sql: + sql = data.get('data') + return sql or '' + + +def _format_sql(sql, data, format='html'): + popts = {} + if data.get('remove_comments'): + popts['strip_comments'] = True + if data.get('keyword_case', 'undefined') not in ('undefined', ''): + popts['keyword_case'] = data.get('keyword_case') + if data.get('identifier_case', 'undefined') not in ('undefined', ''): + popts['identifier_case'] = data.get('identifier_case') + if data.get('n_indents', None) is not None: + val = data.get('n_indents') + try: + popts['indent_width'] = max(1, min(1000, int(val))) + popts['reindent'] = True + except (ValueError, TypeError): + pass + if data.get('output_format', None) is not None: + popts['output_format'] = data.get('output_format') + logging.debug('Format: %s, POPTS: %r', format, popts) + logging.debug(sql) + sql = sqlparse.format(sql, **popts) + if format in ('html', 'json'): + if data.get('highlight', False): + if popts['output_format'] == 'python': + lexer = PythonLexer() + elif popts['output_format'] == 'php': + lexer = PhpLexer() + else: + lexer = SqlLexer() + sql = highlight(sql, lexer, HtmlFormatter()) + else: + sql = ('<textarea class="resizable" ' + 'style="height: 350px; margin-top: 1em;">%s</textarea>' + % sql) + return sql diff --git a/extras/appengine/templates/about.html b/extras/appengine/sqlformat/templates/about.html index 2d4e03e..2d4e03e 100644 --- a/extras/appengine/templates/about.html +++ b/extras/appengine/sqlformat/templates/about.html diff --git a/extras/appengine/templates/api.html b/extras/appengine/sqlformat/templates/api.html index 79bf118..79bf118 100644 --- a/extras/appengine/templates/api.html +++ b/extras/appengine/sqlformat/templates/api.html diff --git a/extras/appengine/templates/index.html b/extras/appengine/sqlformat/templates/index.html index 3b4ea6f..22d6fdb 100644 --- a/extras/appengine/templates/index.html +++ b/extras/appengine/sqlformat/templates/index.html @@ -8,15 +8,13 @@ <form method="post" action="" id="form_options" enctype="multipart/form-data"> <div id="input"> - {% if form.non_field_errors %}{{form.non_field_errors}}{% endif %} <div> <strong>Type your SQL here:</strong><br /> - {{form.data}} - {% if form.data.errors %}{{form.data.errors}}{% endif %} + <textarea id="id_data" rows="10" cols="40" name="data" class="resizable">{{ sql_orig }}</textarea> </div> <div style="margin-top: .5em;"> <strong>...or upload a file:</strong> - {{form.datafile}} + <input type="file" name="datafile" id="id_datafile" /> </div> <div id="examples" style="margin-top: .5em;"></div> <div id="actions" style="margin-top: .5em;"> @@ -50,51 +48,60 @@ <h1 class="skip">Options</h1> <fieldset><legend id="general"><strong>General Options</strong></legend> <div id="general_content" class="content"> - {{form.remove_comments}} - <label for="id_remove_comments">{{form.remove_comments.label}}</label> + <input type="checkbox" id="id_remove_comments" name="remove_comments" value="1" {% if extra.comments %}checked="checked"{% endif %}/> + <label for="id_remove_comments">Remove comments</label> <br /> - {{form.highlight}} - <label for="id_highlight">{{form.highlight.label}}</label> - {% if form.highlight.errors %} - <ul class="errors">{{form.highlight.errors</ul> - {% endif %} + <input type="checkbox" id="id_highlight" name="highlight" value="1" {% if extra.highlight %}checked="checked"{% endif %} /> + <label for="id_highlight">Enable syntax highlighting</label> </div> </fieldset> <fieldset><legend id="kwcase"> <strong>Keywords & Identifiers</strong></legend> - <div> - {{form.keyword_case.label}}: {{form.keyword_case}} + <div> + Keywords: <select id="id_keyword_case" name="keyword_case"> + <option value="">Unchanged</option> + <option value="lower" {% if extra.keywords == 'lower' %}selected="selected"{% endif %}>Lower case</option> + <option value="upper" {% if extra.keywords == 'upper' %}selected="selected"{% endif %}>Upper case</option> + <option value="capitalize" {% if extra.keywords == 'capitalize' %}selected="selected"{% endif %}>Capitalize</option> + </select> </div> <div> - {{form.identifier_case.label}}: {{form.identifier_case}} + Identifiers: <select name="identifier_case" id="id_identifier_case"> + <option value="">Unchanged</option> + <option value="lower" {% if extra.idcase == 'lower' %}selected="selected"{% endif %}>Lower case</option> + <option value="upper" {% if extra.idcase == 'upper' %}selected="selected"{% endif %}>Upper case</option> + <option value="capitalize" {% if extra.idcase == 'capitalize' %}selected="selected"{% endif %}>Capitalize</option> + </select> </div> </fieldset> <fieldset><legend id="indent"><strong>Indentation & Margins</strong> </legend> <div id="indent_content" class="content"> <label for="id_n_indents">Indentation: </label> - {{form.n_indents}} {{form.n_indents.label}} + <input name="n_indents" value="{{extra.n_indents}}" maxlength="2" type="text" id="id_n_indents" size="2" /> spaces <div class="help">Empty field means leave indentation unchanged.</div> -<!-- - <label for="id_right_margin">Right margin: </label> - {{form.right_margin}} {{form.right_margin.label}} - <div class="help">Empty field means don't mind right margin.</div> ---> </div> </fieldset> <fieldset><legend id="output"><strong>Output Format</strong></legend> <label for="id_output_format">Language: </label> - {{form.output_format}} + <select name="output_format" id="id_output_format"> + <option value="sql" {% if extra.lang == 'sql' %}selected="selected"{% endif %}>SQL</option> + <option value="python" {% if extra.lang == 'python' %}selected="selected"{% endif %}>Python</option> + <option value="php" {% if extra.lang == 'php' %}selected="selected"{% endif %}>PHP</option> + </select> </fieldset> <div class="dev">This software is in development.</div> <div> + <g:plusone size="medium"></g:plusone> <a href="http://flattr.com/thing/350724/SQLFormat-Online-SQL-formatting-service" target="_blank"> <img src="http://api.flattr.com/button/flattr-badge-large.png" alt="Flattr this" title="Flattr this" border="0" /> </a> </div> - + <div style="padding-top: 15px;"> + <a href="http://twitter.com/share" class="twitter-share-button" data-count="horizontal" data-via="andialbrecht">Tweet</a><script type="text/javascript" src="http://platform.twitter.com/widgets.js"></script> + </div> </div> <div class="clearfix"></div> </form> diff --git a/extras/appengine/templates/master.html b/extras/appengine/sqlformat/templates/master.html index ea1c662..88a9d36 100644 --- a/extras/appengine/templates/master.html +++ b/extras/appengine/sqlformat/templates/master.html @@ -1,8 +1,8 @@ <html> <head> - <title>{% block title %}SQLFormat - Online SQL Formatting Service{% endblock %}</title> - <meta name="keywords" content="SQL, format, parse, python, beautify, pretty, online, formatting" /> - <meta name="description" content="Easy to use web service to format SQL statements online." /> + <title>{% block title %}SQLFormat - Online SQL Formatter{% endblock %}</title> + <meta name="keywords" content="SQL, format, parse, python, beautify, pretty, online, formatting, formatter" /> + <meta name="description" content="Easy to use web service to format SQL statements." /> <link rel="stylesheet" href="/static/pygments.css" /> <link rel="stylesheet" href="/static/styles.css" /> <script src="http://www.google.com/jsapi"></script> @@ -10,8 +10,6 @@ google.load("jquery", "1.2.6"); </script> <script src="/static/hotkeys.js"></script> - <script type="text/javascript" - src="/static/jquery.textarearesizer.compressed.js"></script> <script src="/static/script.js"></script> </head> <body> @@ -66,7 +64,7 @@ <div id="footer"> <div id="footer-inner"> <div style="float: left; font-size: .85em;"> - <div>© 2009 Andi Albrecht + <div>© 2011 Andi Albrecht <code><albrecht dot andi gmail></code> </div> <div> @@ -99,6 +97,7 @@ pageTracker._trackPageview(); } catch(err) {}</script> <script>init();</script> + <script type="text/javascript" src="https://apis.google.com/js/plusone.js"></script> </body> </html> diff --git a/extras/appengine/templates/python-client-example.html b/extras/appengine/sqlformat/templates/python-client-example.html index 68bf820..68bf820 100644 --- a/extras/appengine/templates/python-client-example.html +++ b/extras/appengine/sqlformat/templates/python-client-example.html diff --git a/extras/appengine/templates/source.html b/extras/appengine/sqlformat/templates/source.html index a0ed89d..a0ed89d 100644 --- a/extras/appengine/templates/source.html +++ b/extras/appengine/sqlformat/templates/source.html diff --git a/extras/appengine/sqlformat/urls.py b/extras/appengine/sqlformat/urls.py deleted file mode 100644 index c83290e..0000000 --- a/extras/appengine/sqlformat/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.conf.urls.defaults import * - -urlpatterns = patterns( - 'sqlformat.views', - (r'^$', 'index'), - (r'^source/$', 'source'), - (r'^about/$', 'about'), - (r'^api/$', 'api'), - (r'^format/$', 'format'), - (r'^load_example', 'load_example'), -) diff --git a/extras/appengine/sqlformat/views.py b/extras/appengine/sqlformat/views.py deleted file mode 100644 index 806496f..0000000 --- a/extras/appengine/sqlformat/views.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- coding: utf-8 -*- - -import logging -import md5 -import os -import sys -import time - -from django import forms -from django.http import HttpResponse -from django.template import loader -from django.utils import simplejson as json - -from google.appengine.api import users - -from pygments import highlight -from pygments.formatters import HtmlFormatter -from pygments.lexers import SqlLexer, PythonLexer, PhpLexer - -import sqlparse - - -INITIAL_SQL = "select * from foo join bar on val1 = val2 where id = 123;" -EXAMPLES_DIR = os.path.join(os.path.dirname(__file__), '../examples') - - -# Custom render_to_response() function to avoid loading django.shortcuts -# since django.shortcuts depends on a lot of Django modules we don't need -# here, e.g. lots of modules from django.db. -def render_to_response(template, params=None): - if params is None: - params = {} - return HttpResponse(loader.render_to_string(template, params)) - - -def _get_user_image(user): - if user is None: - return None - digest = md5.new(user.email().lower()).hexdigest() - if os.environ['SERVER_SOFTWARE'].startswith('Dev'): - host = 'localhost%3A8080' - else: - host = 'sqlformat.appspot.com' - default = 'http%3A%2F%2F'+host+'%2Fstatic%2Fblank.gif' - return 'http://gravatar.com/avatar/%s?s=32&d=%s' % (digest, default) - -def _get_examples(): - fnames = os.listdir(EXAMPLES_DIR) - fnames.sort() - return fnames - - -class FormOptions(forms.Form): - data = forms.CharField(widget=forms.Textarea({'class': 'resizable'}), - initial=INITIAL_SQL, required=False) - datafile = forms.FileField(required=False) - highlight = forms.BooleanField(initial=True, required=False, - widget=forms.CheckboxInput(), - label='Enable syntax highlighting') - remove_comments = forms.BooleanField(initial=False, required=False, - widget=forms.CheckboxInput(), - label='Remove comments') - keyword_case = forms.CharField( - widget=forms.Select(choices=(('', 'Unchanged'), - ('lower', 'Lower case'), - ('upper', 'Upper case'), - ('capitalize', 'Capitalize'))), - required=False, initial='upper', label='Keywords') - identifier_case = forms.CharField( - widget=forms.Select(choices=(('', 'Unchanged'), - ('lower', 'Lower case'), - ('upper', 'Upper case'), - ('capitalize', 'Capitalize'))), - required=False, initial='', label='Identifiers') - n_indents = forms.IntegerField(min_value=1, max_value=30, - initial=2, required=False, - label='spaces', - widget=forms.TextInput({'size': 2, - 'maxlength': 2})) -# right_margin = forms.IntegerField(min_value=10, max_value=500, -# initial=60, required=False, -# label='characters', -# widget=forms.TextInput({'size': 3, -# 'maxlength': 3})) - output_format = forms.CharField( - widget=forms.Select(choices=(('sql', 'SQL'), - ('python', 'Python'), - ('php', 'PHP'), - )), - required=False, initial='sql', label='Language') - - def clean(self): - super(FormOptions, self).clean() - data = self.cleaned_data.get('data') - logging.info(self.files) - if 'datafile' in self.files: - self._datafile = self.files['datafile'].read() - else: - self._datafile = None - if not data and not self._datafile: - raise forms.ValidationError('Whoops, I need a file or text!') - elif data and self._datafile: - raise forms.ValidationError('Whoops, I need a file OR text!') - return self.cleaned_data - - def clean_output_format(self): - frmt = self.cleaned_data.get('output_format') - if not frmt: - frmt = 'sql' - return frmt.lower() - - def get_data(self): - data = self.cleaned_data.get('data') - if self._datafile: - return self._datafile - else: - return data - - -def format_sql(form, format='html'): - data = form.cleaned_data - popts = {} - sql = form.get_data() - if data.get('remove_comments'): - popts['strip_comments'] = True - if data.get('keyword_case'): - popts['keyword_case'] = data.get('keyword_case') - if data.get('identifier_case'): - popts['identifier_case'] = data.get('identifier_case') - if data.get('n_indents', None) is not None: - popts['reindent'] = True - popts['indent_width'] = data.get('n_indents') - if data.get('right_margin', None) is not None: - popts['right_margin'] = data.get('right_margin') - if data.get('output_format', None) is not None: - popts['output_format'] = data.get('output_format') - sql = sqlparse.format(sql, **popts) - if format in ('html', 'json'): - if data.get('highlight', False): - if popts['output_format'] == 'python': - lexer = PythonLexer() - elif popts['output_format'] == 'php': - lexer = PhpLexer() - else: - lexer = SqlLexer() - sql = highlight(sql, lexer, HtmlFormatter()) - else: - sql = ('<textarea class="resizable" ' - 'style="height: 350px; margin-top: 1em;">%s</textarea>' - % sql) - return sql - - -def index(request): - output = None - data = {} - proc_time = None - if request.method == 'POST': - logging.debug(request.POST) - form = FormOptions(request.POST, request.FILES) - if form.is_valid(): - start = time.time() - output = format_sql(form, - format=request.POST.get('format', 'html')) - proc_time = time.time()-start - else: - form = FormOptions() - if request.POST.get('format', None) == 'json': - logging.warning(form.errors) - data['errors'] = str(form.errors) - data['output'] = output - logging.info('%r', proc_time) - data['proc_time'] = '%.3f' % (proc_time or 0.0) - data = json.dumps(data) - return HttpResponse(data, content_type='text/x-json') - elif request.POST.get('format', None) == 'text': - if not form.is_valid(): - data = str(form.errors) # XXX convert to plain text - else: - data = output - return HttpResponse(data, content_type='text/plain') - return render_to_response('index.html', - {'form': form, 'output': output, - 'proc_time': proc_time and '%.3f' % proc_time or None, - 'user': users.get_current_user(), - 'login_url': users.create_login_url('/'), - 'logout_url': users.create_logout_url('/'), - 'userimg': _get_user_image(users.get_current_user()), - 'examples': _get_examples()}) - - -def format(request): - if request.method == 'POST': - form = FormOptions(request.POST) - if form.is_valid(): - try: - response = format_sql(form, format='text') - except: - err = sys.exc_info()[1] - response = 'ERROR: Parsing failed. %s' % str(err) - else: - response = 'ERROR: %s' % str(form.errors) - else: - response = 'POST request required' - return HttpResponse(response, content_type='text/plain') - -def source(request): - return render_to_response('source.html') - -def about(request): - return render_to_response('about.html') - -def api(request): - return render_to_response('api.html') - -def load_example(request): - fname = request.POST.get('fname') - if fname is None: - answer = 'Uups, I\'ve got no filename...' - elif fname not in _get_examples(): - answer = 'Hmm, I think you don\'t want to do that.' - else: - answer = open(os.path.join(EXAMPLES_DIR, fname)).read() - data = json.dumps({'answer': answer}) - return HttpResponse(data, content_type='text/x-json') diff --git a/extras/appengine/static/blank.gif b/extras/appengine/static/blank.gif Binary files differdeleted file mode 100644 index 3be2119..0000000 --- a/extras/appengine/static/blank.gif +++ /dev/null diff --git a/extras/appengine/static/canvas.html b/extras/appengine/static/canvas.html deleted file mode 100644 index ab642d0..0000000 --- a/extras/appengine/static/canvas.html +++ /dev/null @@ -1,114 +0,0 @@ -<html> - -<head> -<style type="text/css"> - /* - These styles are customizable. - Provide .canvas-gadget (the div that holds the canvas mode gadget) - at least 500px width so that the gadget has sufficient screen real estate -*/ -body { - margin: 0; - font-family:arial, sans-serif; - text-align:center; -} -.container { - width:652px; - margin:0 auto; - text-align:left; -} -.fc-sign-in-header { - text-align:left; - font-size: 13px; - padding:3px 10px; - border-bottom:1px solid #000000; -} -.signin { - text-align:left; - float:right; - font-size: 13px; - height: 32px; -} -.go-back { - text-align:left; - margin:5px auto 15px auto; -} -.go-back a, .go-back a:visited { - font-weight:bold; -} -.canvas-gadget { - text-align:left; - width:650px; /* ALLOW AT LEAST 500px WIDTH*/ - margin:10px auto 10px auto; - border:1px solid #cccccc; -} -.site-header { - margin-top: 10px; -} -.section-title { - font-size: 2em; -} -.clear { - clear:both; - font-size:1px; - height:1px; - line-height:0; - margin:0; - padding:0; -} -</style> -<script type="text/javascript" src="http://www.google.com/friendconnect/script/friendconnect.js"></script> -</head> -<body> -<div class="container"> - <div class="fc-sign-in-header"> - <!--REQUIRED SO VISITORS CAN SIGN IN--> - <div class="signin" id="gadget-signin"></div> - <script type="text/javascript"> - var skin = {}; - skin['BORDER_COLOR'] = '#cccccc'; - skin['ENDCAP_BG_COLOR'] = '#e0ecff'; - skin['ENDCAP_TEXT_COLOR'] = '#333333'; - skin['ENDCAP_LINK_COLOR'] = '#0000cc'; - skin['ALTERNATE_BG_COLOR'] = '#ffffff'; - skin['CONTENT_BG_COLOR'] = '#ffffff'; - skin['CONTENT_LINK_COLOR'] = '#0000cc'; - skin['CONTENT_TEXT_COLOR'] = '#333333'; - skin['CONTENT_SECONDARY_LINK_COLOR'] = '#7777cc'; - skin['CONTENT_SECONDARY_TEXT_COLOR'] = '#666666'; - skin['CONTENT_HEADLINE_COLOR'] = '#333333'; - skin['ALIGNMENT'] = 'right'; - google.friendconnect.container.renderCanvasSignInGadget({'id': 'gadget-signin'}, skin); - </script> - <!--END REQUIRED--> - <div class="clear"></div> - </div> - - <div class="site-header"><span class="section-title">Site Name</span></div> - <div class="go-back"> - <!--REQUIRED SO VISITORS CAN RETURN TO REFERRING PAGE--> - <a href="javascript:google.friendconnect.container.goBackToSite();"> - ‹‹ Return home</a> - <!--END REQUIRED--> - </div> - <!-- REQUIRED - THIS IS WHERE THE GADGET IS PRESENTED. ALLOW AT LEAST 500px WIDTH --> - <div id="gadget-canvas" class="canvas-gadget"></div> - <script type="text/javascript"> - var skin = {}; - skin['BORDER_COLOR'] = '#cccccc'; - skin['ENDCAP_BG_COLOR'] = '#e0ecff'; - skin['ENDCAP_TEXT_COLOR'] = '#333333'; - skin['ENDCAP_LINK_COLOR'] = '#0000cc'; - skin['ALTERNATE_BG_COLOR'] = '#ffffff'; - skin['CONTENT_BG_COLOR'] = '#ffffff'; - skin['CONTENT_LINK_COLOR'] = '#0000cc'; - skin['CONTENT_TEXT_COLOR'] = '#333333'; - skin['CONTENT_SECONDARY_LINK_COLOR'] = '#7777cc'; - skin['CONTENT_SECONDARY_TEXT_COLOR'] = '#666666'; - skin['CONTENT_HEADLINE_COLOR'] = '#333333'; - google.friendconnect.container.renderUrlCanvasGadget({'id': 'gadget-canvas'}, skin); - </script> - <!--END REQUIRED--> -</div> -</body> -</html>
\ No newline at end of file diff --git a/extras/appengine/static/favicon.ico b/extras/appengine/static/favicon.ico Binary files differnew file mode 100644 index 0000000..1372520 --- /dev/null +++ b/extras/appengine/static/favicon.ico diff --git a/extras/appengine/static/img_loading.gif b/extras/appengine/static/img_loading.gif Binary files differdeleted file mode 100644 index 6465823..0000000 --- a/extras/appengine/static/img_loading.gif +++ /dev/null diff --git a/extras/appengine/static/jquery.textarearesizer.compressed.js b/extras/appengine/static/jquery.textarearesizer.compressed.js deleted file mode 100644 index 5464ae6..0000000 --- a/extras/appengine/static/jquery.textarearesizer.compressed.js +++ /dev/null @@ -1 +0,0 @@ -(function($){var textarea,staticOffset;var iLastMousePos=0;var iMin=32;var grip;$.fn.TextAreaResizer=function(){return this.each(function(){textarea=$(this).addClass('processed'),staticOffset=null;$(this).wrap('<div class="resizable-textarea"><span></span></div>').parent().append($('<div class="grippie"></div>').bind("mousedown",{el:this},startDrag));var grippie=$('div.grippie',$(this).parent())[0];grippie.style.marginRight=(grippie.offsetWidth-$(this)[0].offsetWidth)+'px'})};function startDrag(e){textarea=$(e.data.el);textarea.blur();iLastMousePos=mousePosition(e).y;staticOffset=textarea.height()-iLastMousePos;textarea.css('opacity',0.25);$(document).mousemove(performDrag).mouseup(endDrag);return false}function performDrag(e){var iThisMousePos=mousePosition(e).y;var iMousePos=staticOffset+iThisMousePos;if(iLastMousePos>=(iThisMousePos)){iMousePos-=5}iLastMousePos=iThisMousePos;iMousePos=Math.max(iMin,iMousePos);textarea.height(iMousePos+'px');if(iMousePos<iMin){endDrag(e)}return false}function endDrag(e){$(document).unbind('mousemove',performDrag).unbind('mouseup',endDrag);textarea.css('opacity',1);textarea.focus();textarea=null;staticOffset=null;iLastMousePos=0}function mousePosition(e){return{x:e.clientX+document.documentElement.scrollLeft,y:e.clientY+document.documentElement.scrollTop}}})(jQuery);
\ No newline at end of file diff --git a/extras/appengine/static/resize-grip.png b/extras/appengine/static/resize-grip.png Binary files differdeleted file mode 100644 index cae2a4e..0000000 --- a/extras/appengine/static/resize-grip.png +++ /dev/null diff --git a/extras/appengine/static/rpc_relay.html b/extras/appengine/static/rpc_relay.html deleted file mode 100644 index c602043..0000000 --- a/extras/appengine/static/rpc_relay.html +++ /dev/null @@ -1 +0,0 @@ -<html><head><script type="text/javascript" src="http://www.google.com/friendconnect/script/rpc_relay.js"></script></head></html>
\ No newline at end of file diff --git a/extras/appengine/static/script.js b/extras/appengine/static/script.js index 71bbabb..8bdf271 100644 --- a/extras/appengine/static/script.js +++ b/extras/appengine/static/script.js @@ -11,7 +11,6 @@ function update_output() { data.keyword_case = $('#id_keyword_case').val(); data.identifier_case = $('#id_identifier_case').val(); data.n_indents = $('#id_n_indents').val(); - data.right_margin = $('#id_right_margin').val(); data.output_format = $('#id_output_format').val(); form = document.getElementById('form_options'); $(form.elements).attr('disabled', 'disabled'); @@ -72,7 +71,7 @@ function load_example() { fname = $('#sel_example').val(); data = {fname: fname}; $.post('/load_example', data, - function(data) { + function(data) { $('#id_data').val(data.answer); }, 'json'); } @@ -96,8 +95,4 @@ function init() { $(document).bind('keydown', {combi: 't', disableInInput: true}, textarea_grab_focus); initialized = true; - /* jQuery textarea resizer plugin usage */ - $(document).ready(function() { - $('textarea.resizable:not(.processed)').TextAreaResizer(); - }); }
\ No newline at end of file diff --git a/extras/appengine/static/sqlformat_client_example.py b/extras/appengine/static/sqlformat_client_example.py index eec17b9..8b2a9e9 100644 --- a/extras/appengine/static/sqlformat_client_example.py +++ b/extras/appengine/static/sqlformat_client_example.py @@ -13,6 +13,7 @@ payload = ( ('n_indents', 2), ) + response = urllib2.urlopen(REMOTE_API, urllib.urlencode(payload)) print response.read() diff --git a/extras/appengine/templates/404.html b/extras/appengine/templates/404.html deleted file mode 100644 index 1eb60e8..0000000 --- a/extras/appengine/templates/404.html +++ /dev/null @@ -1,12 +0,0 @@ -<html> - <head><title>404 Not Found</title></head> - <body> - <h1>404 - Not Found.</h1> - <p> - If you think this is a bug please file an issue on the tracker: - <a href="http://code.google.com/p/python-sqlparse/issues/list"> - http://code.google.com/p/python-sqlparse/issues/list - </a>. - </p> - </body> -</html> diff --git a/extras/appengine/templates/500.html b/extras/appengine/templates/500.html deleted file mode 100644 index 8fa8f56..0000000 --- a/extras/appengine/templates/500.html +++ /dev/null @@ -1,19 +0,0 @@ -<html> - <head><title>500 Internal Server Error</title></head> - <body> - <h1>uups... 500 Internal Server Error.</h1> - <p> - Looks like you've hit a bug! Please file an issue on the tracker: - <a href="http://code.google.com/p/python-sqlparse/issues/list"> - http://code.google.com/p/python-sqlparse/issues/list - </a>. - </p> - <p> - Please add a short description what happened and if possible add the SQL - statement you've tried to format. - </p> - <p> - Thanks! - </p> - </body> -</html> diff --git a/extras/py3k/Makefile b/extras/py3k/Makefile index 499f2bf..57fcaeb 100644 --- a/extras/py3k/Makefile +++ b/extras/py3k/Makefile @@ -1,17 +1,16 @@ -2TO3=2to3-3.1 +2TO3=2to3 +2TO3OPTS=--no-diffs -w -n all: sqlparse tests sqlparse: cp -r ../../sqlparse . - $(2TO3) sqlparse > sqlparse.diff - patch -p0 < sqlparse.diff + $(2TO3) $(2TO3OPTS) sqlparse patch -p0 < fixes.diff tests: cp -r ../../tests . - $(2TO3) tests > tests.diff - patch -p0 < tests.diff + $(2TO3) $(2TO3OPTS) tests clean: rm -rf sqlparse diff --git a/sqlparse/__init__.py b/sqlparse/__init__.py index 903fdd1..7698e46 100644 --- a/sqlparse/__init__.py +++ b/sqlparse/__init__.py @@ -6,7 +6,7 @@ """Parse SQL statements.""" -__version__ = '0.1.2' +__version__ = '0.1.3' class SQLParseError(Exception): diff --git a/sqlparse/lexer.py b/sqlparse/lexer.py index 950ef1b..8929e3e 100644 --- a/sqlparse/lexer.py +++ b/sqlparse/lexer.py @@ -150,7 +150,7 @@ class LexerMeta(type): return type.__call__(cls, *args, **kwds) -class Lexer: +class Lexer(object): __metaclass__ = LexerMeta @@ -189,6 +189,7 @@ class Lexer: (r"(''|'.*?[^\\]')", tokens.String.Single), # not a real string literal in ANSI SQL: (r'(""|".*?[^\\]")', tokens.String.Symbol), + (r'(\[.*[^\]]\])', tokens.Name), (r'(LEFT |RIGHT )?(INNER |OUTER )?JOIN\b', tokens.Keyword), (r'END( IF| LOOP)?\b', tokens.Keyword), (r'NOT NULL\b', tokens.Keyword), diff --git a/tests/test_parse.py b/tests/test_parse.py index 6ff8dd8..5f9bb2d 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -95,3 +95,10 @@ class SQLParseTest(TestCaseBase): self.assert_(t[-1].ttype is sqlparse.tokens.Name.Placeholder) self.assertEqual(t[-1].value, '$a') + def test_access_symbol(self): # see issue27 + t = sqlparse.parse('select a.[foo bar] as foo')[0].tokens + self.assert_(isinstance(t[-1], sqlparse.sql.Identifier)) + self.assertEqual(t[-1].get_name(), 'foo') + self.assertEqual(t[-1].get_real_name(), '[foo bar]') + self.assertEqual(t[-1].get_parent_name(), 'a') + diff --git a/tests/utils.py b/tests/utils.py index 6135dc3..e2c01a3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -33,6 +33,7 @@ class TestCaseBase(unittest.TestCase): ssecond = unicode(second) diff = difflib.ndiff(sfirst.splitlines(), ssecond.splitlines()) fp = StringIO() - print >> fp, NL, NL.join(diff) + fp.write(NL) + fp.write(NL.join(diff)) print fp.getvalue() raise self.failureException, fp.getvalue() |
