From 2ed9aae303e8d78683c498152afda9f74a494e91 Mon Sep 17 00:00:00 2001 From: Alice Bevan-McGregor Date: Wed, 21 Nov 2018 11:56:08 -0500 Subject: [PATCH 01/14] Updated benchmark to include Genshi; retabbed. --- example/benchmark.py | 662 ++++++++++++++++++++++--------------------- 1 file changed, 341 insertions(+), 321 deletions(-) diff --git a/example/benchmark.py b/example/benchmark.py index cfd9ad3..4c051f2 100644 --- a/example/benchmark.py +++ b/example/benchmark.py @@ -7,482 +7,502 @@ import sys try: - from wheezy.html.utils import escape_html as escape + from wheezy.html.utils import escape_html as escape except ImportError: - import cgi - escape = cgi.escape + import cgi + escape = cgi.escape PY3 = sys.version_info[0] >= 3 s = PY3 and str or unicode ctx = { - 'table': [dict(a=1, b=2, c=3, d=4, e=5, f=6, g=7, h=8, i=9, j=10) - for x in range(1000)] + 'table': [dict(a=1, b=2, c=3, d=4, e=5, f=6, g=7, h=8, i=9, j=10) + for x in range(1000)] } # region: python list append if PY3: - def test_list_append(): - b = [] - w = b.append - table = ctx['table'] - w('\n') - for row in table: - w('\n') - for key, value in row.items(): - w('\n') - w('\n') - w('
') - w(escape(key)) - w('') - w(str(value)) - w('
') - return ''.join(b) + def test_list_append(): + b = [] + w = b.append + table = ctx['table'] + w('\n') + for row in table: + w('\n') + for key, value in row.items(): + w('\n') + w('\n') + w('
') + w(escape(key)) + w('') + w(str(value)) + w('
') + return ''.join(b) else: - def test_list_append(): # noqa - b = [] - w = b.append - table = ctx['table'] - w(u'\n') - for row in table: - w(u'\n') - for key, value in row.items(): - w(u'\n') - w(u'\n') - w(u'
') - w(escape(key)) - w(u'') - w(unicode(value)) - w(u'
') - return ''.join(b) + def test_list_append(): # noqa + b = [] + w = b.append + table = ctx['table'] + w(u'\n') + for row in table: + w(u'\n') + for key, value in row.items(): + w(u'\n') + w(u'\n') + w(u'
') + w(escape(key)) + w(u'') + w(unicode(value)) + w(u'
') + return ''.join(b) # region: python list extend if PY3: - def test_list_extend(): - b = [] - e = b.extend - table = ctx['table'] - e(('\n',)) - for row in table: - e(('\n',)) - for key, value in row.items(): - e(('\n')) - e(('\n',)) - e(('
', - escape(key), - '', - str(value), - '
',)) - return ''.join(b) + def test_list_extend(): + b = [] + e = b.extend + table = ctx['table'] + e(('\n',)) + for row in table: + e(('\n',)) + for key, value in row.items(): + e(('\n')) + e(('\n',)) + e(('
', + escape(key), + '', + str(value), + '
',)) + return ''.join(b) else: - def test_list_extend(): # noqa - b = [] - e = b.extend - table = ctx['table'] - e((u'\n',)) - for row in table: - e((u'\n',)) - for key, value in row.items(): - e((u'\n')) - e((u'\n',)) - e((u'
', - escape(key), - u'', - unicode(value), - u'
',)) - return ''.join(b) + def test_list_extend(): # noqa + b = [] + e = b.extend + table = ctx['table'] + e((u'\n',)) + for row in table: + e((u'\n',)) + for key, value in row.items(): + e((u'\n')) + e((u'\n',)) + e((u'
', + escape(key), + u'', + unicode(value), + u'
',)) + return ''.join(b) # region: wheezy.template try: - from wheezy.template.engine import Engine - from wheezy.template.loader import DictLoader - from wheezy.template.ext.core import CoreExtension + from wheezy.template.engine import Engine + from wheezy.template.loader import DictLoader + from wheezy.template.ext.core import CoreExtension except ImportError: - test_wheezy_template = None + test_wheezy_template = None else: - engine = Engine(loader=DictLoader({'x': s("""\ + engine = Engine(loader=DictLoader({'x': s("""\ @require(table) - @for row in table: - - @for key, value in row.items(): - - @end - - @end + @for row in table: + + @for key, value in row.items(): + + @end + + @end
@key!h@value!s
@key!h@value!s
""")}), extensions=[CoreExtension()]) - engine.global_vars.update({'h': escape}) - wheezy_template = engine.get_template('x') + engine.global_vars.update({'h': escape}) + wheezy_template = engine.get_template('x') - def test_wheezy_template(): - return wheezy_template.render(ctx) + def test_wheezy_template(): + return wheezy_template.render(ctx) # region: Jinja2 try: - from jinja2 import Environment + from jinja2 import Environment except ImportError: - test_jinja2 = None + test_jinja2 = None else: - jinja2_template = Environment().from_string(s("""\ + jinja2_template = Environment().from_string(s("""\ - {% for row in table: %} - - {% for key, value in row.items(): %} - - {% endfor %} - - {% endfor %} + {% for row in table: %} + + {% for key, value in row.items(): %} + + {% endfor %} + + {% endfor %}
{{ key | e }}{{ value }}
{{ key | e }}{{ value }}
""")) - def test_jinja2(): - return jinja2_template.render(ctx) + def test_jinja2(): + return jinja2_template.render(ctx) # region: tornado try: - from tornado.template import Template + from tornado.template import Template except ImportError: - test_tornado = None + test_tornado = None else: - tornado_template = Template(s("""\ + tornado_template = Template(s("""\ - {% for row in table %} - - {% for key, value in row.items() %} - - {% end %} - - {% end %} + {% for row in table %} + + {% for key, value in row.items() %} + + {% end %} + + {% end %}
{{ key }}{{ value }}
{{ key }}{{ value }}
""")) - def test_tornado(): - return tornado_template.generate(**ctx).decode('utf8') + def test_tornado(): + return tornado_template.generate(**ctx).decode('utf8') # region: mako try: - from mako.template import Template + from mako.template import Template except ImportError: - test_mako = None + test_mako = None else: - mako_template = Template(s("""\ + mako_template = Template(s("""\ - % for row in table: - - % for key, value in row.items(): - - % endfor - - % endfor + % for row in table: + + % for key, value in row.items(): + + % endfor + + % endfor
${ key | h }${ value }
${ key | h }${ value }
""")) - def test_mako(): - return mako_template.render(**ctx) + def test_mako(): + return mako_template.render(**ctx) # region: tenjin try: - import tenjin + import tenjin except ImportError: - test_tenjin = None + test_tenjin = None else: - try: - import webext - helpers = { - 'to_str': webext.to_str, - 'escape': webext.escape_html - } - except ImportError: - helpers = { - 'to_str': tenjin.helpers.to_str, - 'escape': tenjin.helpers.escape - } - nop_helpers = { - 'to_str': str, - 'escape': str - } - tenjin_template = tenjin.Template(encoding='utf8') - tenjin_template.convert(s("""\ + try: + import webext + helpers = { + 'to_str': webext.to_str, + 'escape': webext.escape_html + } + except ImportError: + helpers = { + 'to_str': tenjin.helpers.to_str, + 'escape': tenjin.helpers.escape + } + nop_helpers = { + 'to_str': str, + 'escape': str + } + tenjin_template = tenjin.Template(encoding='utf8') + tenjin_template.convert(s("""\ - - - - - - - + + + + + + +
${ key }#{ value }
${ key }#{ value }
""")) - def test_tenjin(): - return tenjin_template.render(ctx, helpers) + def test_tenjin(): + return tenjin_template.render(ctx, helpers) - def test_tenjin_unsafe(): - return tenjin_template.render(ctx, nop_helpers) + def test_tenjin_unsafe(): + return tenjin_template.render(ctx, nop_helpers) # region: web2py try: - import cStringIO - from gluon.html import xmlescape - from gluon.template import get_parsed + import cStringIO + from gluon.html import xmlescape + from gluon.template import get_parsed except ImportError: - test_web2py = None + test_web2py = None else: - # see gluon.globals.Response - class DummyResponse(object): - def __init__(self): - self.body = cStringIO.StringIO() - - def write(self, data, escape=True): - if not escape: - self.body.write(str(data)) - else: - self.body.write(xmlescape(data)) - - web2py_template = compile(get_parsed(s("""\ + # see gluon.globals.Response + class DummyResponse(object): + def __init__(self): + self.body = cStringIO.StringIO() + + def write(self, data, escape=True): + if not escape: + self.body.write(str(data)) + else: + self.body.write(xmlescape(data)) + + web2py_template = compile(get_parsed(s("""\ - {{ for row in table: }} - - {{ for key, value in row.items(): }} - - {{ pass }} - - {{ pass }} + {{ for row in table: }} + + {{ for key, value in row.items(): }} + + {{ pass }} + + {{ pass }}
{{ =key }}{{ =value }}
{{ =key }}{{ =value }}
""")), '', 'exec') - def test_web2py(): - response = DummyResponse() - exec(web2py_template, {}, dict(response=response, **ctx)) - return response.body.getvalue().decode('utf8') + def test_web2py(): + response = DummyResponse() + exec(web2py_template, {}, dict(response=response, **ctx)) + return response.body.getvalue().decode('utf8') # region: django try: - from django.conf import settings - settings.configure() - from django.template import Template - from django.template import Context + from django.conf import settings + settings.configure() + from django.template import Template + from django.template import Context except ImportError: - test_django = None + test_django = None else: - django_template = Template(s("""\ + django_template = Template(s("""\ - {% for row in table %} - - {% for key, value in row.items %} - - {% endfor %} - - {% endfor %} + {% for row in table %} + + {% for key, value in row.items %} + + {% endfor %} + + {% endfor %}
{{ key }}{{ value }}
{{ key }}{{ value }}
""")) - def test_django(): - return django_template.render(Context(ctx)) + def test_django(): + return django_template.render(Context(ctx)) # region: chameleon try: - from chameleon.zpt.template import PageTemplate + from chameleon.zpt.template import PageTemplate except ImportError: - test_chameleon = None + test_chameleon = None else: - chameleon_template = PageTemplate(s("""\ + chameleon_template = PageTemplate(s("""\ - - - - - + + + + +
${key}${row[key]}
${key}${row[key]}
""")) - def test_chameleon(): - return chameleon_template.render(**ctx) + def test_chameleon(): + return chameleon_template.render(**ctx) # region: cheetah try: - from Cheetah.Template import Template + from Cheetah.Template import Template except ImportError: - test_cheetah = None + test_cheetah = None else: - cheetah_ctx = {} - cheetah_template = Template(s("""\ + cheetah_ctx = {} + cheetah_template = Template(s("""\ #import cgi - #for $row in $table - - #for $key, $value in $row.items - - #end for - - #end for + #for $row in $table + + #for $key, $value in $row.items + + #end for + + #end for
$cgi.escape($key)$value
$cgi.escape($key)$value
"""), searchList=[cheetah_ctx]) - def test_cheetah(): - cheetah_ctx.update(ctx) - output = cheetah_template.respond() - cheetah_ctx.clear() - return output + def test_cheetah(): + cheetah_ctx.update(ctx) + output = cheetah_template.respond() + cheetah_ctx.clear() + return output # region: spitfire try: - import spitfire - import spitfire.compiler.util + import spitfire + import spitfire.compiler.util except ImportError: - test_spitfire = None + test_spitfire = None else: - spitfire_template = spitfire.compiler.util.load_template(s("""\ + spitfire_template = spitfire.compiler.util.load_template(s("""\ #from cgi import escape - #for $row in $table - - #for $key, $value in $row.items() - - #end for - - #end for + #for $row in $table + + #for $key, $value in $row.items() + + #end for + + #end for
${key|filter=escape}$value
${key|filter=escape}$value
"""), 'spitfire_template', spitfire.compiler.analyzer.o3_options, { - 'enable_filters': True}) + 'enable_filters': True}) - def test_spitfire(): - return spitfire_template(search_list=[ctx]).main() + def test_spitfire(): + return spitfire_template(search_list=[ctx]).main() # region: qpy try: - from qpy import join_xml - from qpy import xml - from qpy import xml_quote + from qpy import join_xml + from qpy import xml + from qpy import xml_quote except ImportError: - test_qpy_list_append = None + test_qpy_list_append = None else: - if PY3: - def test_qpy_list_append(): - b = [] - w = b.append - table = ctx['table'] - w(xml('\n')) - for row in table: - w(xml('\n')) - for key, value in row.items(): - w(xml('\n')) - w(xml('\n')) - w(xml('
')) - w(xml_quote(key)) - w(xml('')) - w(value) - w(xml('
')) - return join_xml(b) - else: - def test_qpy_list_append(): - b = [] - w = b.append - table = ctx['table'] - w(xml(u'\n')) - for row in table: - w(xml(u'\n')) - for key, value in row.items(): - w(xml(u'\n')) - w(xml(u'\n')) - w(xml(u'
')) - w(xml_quote(key)) - w(xml(u'')) - w(value) - w(xml(u'
')) - return join_xml(b) + if PY3: + def test_qpy_list_append(): + b = [] + w = b.append + table = ctx['table'] + w(xml('\n')) + for row in table: + w(xml('\n')) + for key, value in row.items(): + w(xml('\n')) + w(xml('\n')) + w(xml('
')) + w(xml_quote(key)) + w(xml('')) + w(value) + w(xml('
')) + return join_xml(b) + else: + def test_qpy_list_append(): + b = [] + w = b.append + table = ctx['table'] + w(xml(u'\n')) + for row in table: + w(xml(u'\n')) + for key, value in row.items(): + w(xml(u'\n')) + w(xml(u'\n')) + w(xml(u'
')) + w(xml_quote(key)) + w(xml(u'')) + w(value) + w(xml(u'
')) + return join_xml(b) # region: bottle try: - from bottle import SimpleTemplate + from bottle import SimpleTemplate except ImportError: - test_bottle = None + test_bottle = None else: - bottle_template = SimpleTemplate(s("""\ + bottle_template = SimpleTemplate(s("""\ - % for row in table: - - % for key, value in row.items(): - - % end - - % end + % for row in table: + + % for key, value in row.items(): + + % end + + % end
{{key}}{{!value}}
{{key}}{{!value}}
""")) - def test_bottle(): - return bottle_template.render(**ctx) + def test_bottle(): + return bottle_template.render(**ctx) # region: cinje try: - import cinje - import bigtable + import cinje + import bigtable except ImportError: - test_cinje = None + test_cinje = None else: - def test_cinje(): - return ''.join(bigtable.bigtable(table=bigtable.table)) - def test_cinje_unsafe(): - return ''.join(bigtable.bigtable_unsafe(table=bigtable.table)) - def test_cinje_flush_first(): - return next(bigtable.bigtable_stream(table=bigtable.table)) - def test_cinje_flush_all(): - return ''.join(bigtable.bigtable_stream(table=bigtable.table)) - def test_cinje_fancy_first(): - return next(bigtable.bigtable_fancy(table=bigtable.table)) - def test_cinje_fancy_all(): - return ''.join(bigtable.bigtable_fancy(table=bigtable.table)) + def test_cinje(): + return ''.join(bigtable.bigtable(table=bigtable.table)) + def test_cinje_unsafe(): + return ''.join(bigtable.bigtable_unsafe(table=bigtable.table)) + def test_cinje_flush_first(): + return next(bigtable.bigtable_stream(table=bigtable.table)) + def test_cinje_flush_all(): + return ''.join(bigtable.bigtable_stream(table=bigtable.table)) + def test_cinje_fancy_first(): + return next(bigtable.bigtable_fancy(table=bigtable.table)) + def test_cinje_fancy_all(): + return ''.join(bigtable.bigtable_fancy(table=bigtable.table)) + + +# region: genshi + +try: + from genshi.template import MarkupTemplate +except ImportError: + test_genshi = None +else: + genshi_template = MarkupTemplate(""" + + + + + +
${key}${row[key]}
""") + + def test_genshi(): + result = genshi_template.generate(**ctx) + return str(result) def run(number=100): - import profile - from timeit import Timer - from pstats import Stats - names = globals().keys() - names = sorted([(name, globals()[name]) - for name in names if name.startswith('test_')]) - print(" msec rps tcalls funcs") - for name, test in names: - if test: - assert isinstance(test(), s) - t = Timer(setup='from __main__ import %s as t' % name, - stmt='t()') - t = t.timeit(number=number) - st = Stats(profile.Profile().runctx( - 'test()', globals(), locals())) - print('%-17s %7.2f %6.2f %7d %6d' % (name[5:], - 1000 * t / number, - number / t, - st.total_calls, - len(st.stats))) - else: - print('%-26s not installed' % name[5:]) + import profile + from timeit import Timer + from pstats import Stats + names = globals().keys() + names = sorted([(name, globals()[name]) + for name in names if name.startswith('test_')]) + print(" msec rps tcalls funcs") + for name, test in names: + if test: + assert isinstance(test(), s) + t = Timer(setup='from __main__ import %s as t' % name, + stmt='t()') + t = t.timeit(number=number) + st = Stats(profile.Profile().runctx( + 'test()', globals(), locals())) + print('%-17s %7.2f %6.2f %7d %6d' % (name[5:], + 1000 * t / number, + number / t, + st.total_calls, + len(st.stats))) + else: + print('%-26s not installed' % name[5:]) if __name__ == '__main__': - run() + run() From 6a2d3a003cb777fbdd96f37d54d603ee05de28e3 Mon Sep 17 00:00:00 2001 From: Alice Bevan-McGregor Date: Wed, 6 Mar 2019 12:28:02 -0500 Subject: [PATCH 02/14] Added Python 3.7 to the build matrix. --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9d82ba9..a7eab4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: python -sudo: false cache: pip branches: @@ -14,6 +13,11 @@ python: - "3.5" - "3.6" +matrix: + include: + - python: "3.7" + dist: xenial + install: - travis_retry pip install --upgrade setuptools pip codecov - pip install -e '.[development]' From d8df3707866f300ad83b1ca18df21f03555dc452 Mon Sep 17 00:00:00 2001 From: Alice Bevan-McGregor Date: Wed, 6 Mar 2019 14:37:15 -0500 Subject: [PATCH 03/14] Work on #28, armour against StopIteration usage in generators. --- cinje/block/function.py | 6 +++++- cinje/block/generic.py | 6 +++++- cinje/block/using.py | 5 ++++- cinje/inline/blank.py | 5 ++++- cinje/inline/code.py | 5 ++++- cinje/inline/comment.py | 5 ++++- cinje/inline/flush.py | 7 ++++++- cinje/inline/require.py | 6 +++++- cinje/inline/text.py | 17 ++++++++++++++--- cinje/inline/use.py | 6 +++++- 10 files changed, 56 insertions(+), 12 deletions(-) diff --git a/cinje/block/function.py b/cinje/block/function.py index 1826ce1..73f5ff0 100644 --- a/cinje/block/function.py +++ b/cinje/block/function.py @@ -78,7 +78,11 @@ def _optimize(self, context, argspec): def __call__(self, context): input = context.input - declaration = input.next() + try: + declaration = input.next() + except StopIteration: + return + line = declaration.partitioned[1] # We don't care about the "def". line, _, annotation = line.rpartition('->') diff --git a/cinje/block/generic.py b/cinje/block/generic.py index 3c91265..3d8811a 100644 --- a/cinje/block/generic.py +++ b/cinje/block/generic.py @@ -63,7 +63,11 @@ def __call__(self, context): input = context.input - declaration = input.next() + try: + declaration = input.next() + except StopIteration: + return + stripped = declaration.stripped prefix, _ = declaration.partitioned diff --git a/cinje/block/using.py b/cinje/block/using.py index 2682a55..91eafde 100644 --- a/cinje/block/using.py +++ b/cinje/block/using.py @@ -12,7 +12,10 @@ def match(self, context, line): def __call__(self, context): input = context.input - declaration = input.next() + try: + declaration = input.next() + except: + return _, _, declaration = declaration.stripped.partition(' ') name, _, args = declaration.partition(' ') diff --git a/cinje/inline/blank.py b/cinje/inline/blank.py index 4e981f9..eaf1294 100644 --- a/cinje/inline/blank.py +++ b/cinje/inline/blank.py @@ -10,4 +10,7 @@ def match(self, context, line): return not line.stripped def __call__(self, context): - yield context.input.next() + try: + yield context.input.next() + except StopIteration: + return diff --git a/cinje/inline/code.py b/cinje/inline/code.py index d569f56..c4c2729 100644 --- a/cinje/inline/code.py +++ b/cinje/inline/code.py @@ -18,4 +18,7 @@ def match(self, context, line): return line.kind == 'code' def __call__(self, context): - yield context.input.next() # Pass through. + try: + yield context.input.next() # Pass through. + except StopIteration: + return diff --git a/cinje/inline/comment.py b/cinje/inline/comment.py index 76aad49..80058a7 100644 --- a/cinje/inline/comment.py +++ b/cinje/inline/comment.py @@ -23,7 +23,10 @@ def match(self, context, line): def __call__(self, context): """Emit comments into the final code that aren't marked as hidden/private.""" - line = context.input.next() + try: + line = context.input.next() + except StopIteration: + return if not line.stripped.startswith('##'): yield line diff --git a/cinje/inline/flush.py b/cinje/inline/flush.py index e6bd58c..1665541 100644 --- a/cinje/inline/flush.py +++ b/cinje/inline/flush.py @@ -51,4 +51,9 @@ def match(self, context, line): return line.kind == 'code' and line.stripped in ("flush", "yield") def __call__(self, context): - return flush_template(context, context.input.next()) + try: + line = context.input.next() + except StopIteration: + return + + return flush_template(context, line) diff --git a/cinje/inline/require.py b/cinje/inline/require.py index 9649902..4fe7cdb 100644 --- a/cinje/inline/require.py +++ b/cinje/inline/require.py @@ -26,7 +26,11 @@ def __call__(self, context): input = context.input - declaration = input.next() + try: + declaration = input.next() + except StopIteration: + return + namespace = declaration.partitioned[1] # Ignore the "require" part, we care about the namepsace. module = import_module(namespace) diff --git a/cinje/inline/text.py b/cinje/inline/text.py index da3186d..074e18d 100644 --- a/cinje/inline/text.py +++ b/cinje/inline/text.py @@ -73,7 +73,11 @@ def wrap(scope, lines, format=BARE_FORMAT): def gather(input): """Collect contiguous lines of text, preserving line numbers.""" - line = input.next() + try: + line = input.next() + except StopIteration: + return + lead = True buffer = [] @@ -137,7 +141,10 @@ def process(self, context, lines): handler = getattr(self, 'process_' + chunk.kind, self.process_generic)(chunk.kind, context) handler = (chunk.kind, handler) - next(handler[1]) # We fast-forward to the first yield. + try: + next(handler[1]) # We fast-forward to the first yield. + except StopIteration: + return result = handler[1].send(chunk) # Send the handler the next contiguous chunk. if result: yield result @@ -147,7 +154,11 @@ def process(self, context, lines): # Clean up the final iteration. if handler: - result = next(handler[1]) + try: + result = next(handler[1]) + except StopIteration: + return + if result: yield result def process_text(self, kind, context): diff --git a/cinje/inline/use.py b/cinje/inline/use.py index 61d415a..5271e02 100644 --- a/cinje/inline/use.py +++ b/cinje/inline/use.py @@ -29,7 +29,11 @@ def __call__(self, context): input = context.input - declaration = input.next() + try: + declaration = input.next() + except StopIteration: + return + parts = declaration.partitioned[1] # Ignore the "use" part, we care about the name and arguments. name, _, args = parts.partition(' ') From 16fdba97e2907b50a64ac82b0eff26c55fb3d2fb Mon Sep 17 00:00:00 2001 From: Alice Bevan-McGregor Date: Wed, 6 Mar 2019 15:09:11 -0500 Subject: [PATCH 04/14] Added keep files for required (empty) directory structure. --- .packaging/build/.keep | 0 .packaging/dist/.keep | 0 .packaging/release/.keep | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 .packaging/build/.keep create mode 100644 .packaging/dist/.keep create mode 100644 .packaging/release/.keep diff --git a/.packaging/build/.keep b/.packaging/build/.keep new file mode 100644 index 0000000..e69de29 diff --git a/.packaging/dist/.keep b/.packaging/dist/.keep new file mode 100644 index 0000000..e69de29 diff --git a/.packaging/release/.keep b/.packaging/release/.keep new file mode 100644 index 0000000..e69de29 From c198a306fad2bac05550b51bdce86067543f4d89 Mon Sep 17 00:00:00 2001 From: Alice Bevan-McGregor Date: Wed, 6 Mar 2019 16:42:08 -0500 Subject: [PATCH 05/14] Version and copyright bump, release notes. --- LICENSE.txt | 2 +- README.rst | 18 ++++++++++++++---- cinje/release.py | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 491ef77..e98de42 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright © 2006-2018 Alice Bevan-McGregor and contributors. +Copyright © 2006-2019 Alice Bevan-McGregor and contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.rst b/README.rst index 03d9070..89d074d 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ cinje ===== - © 2015-2017 Alice Bevan-McGregor and contributors. + © 2015-2019 Alice Bevan-McGregor and contributors. .. @@ -580,6 +580,15 @@ Just like with ``using``, the result of the expression must be a callable genera Version History =============== +Version 1.1.2 +------------- + +* *Fixed* `Python 3.7 exception use within generators. `_ + +* *Added* Genshi to the `benchmark comparison suite `_. + +* *Fixed* minor docstring typo. + Version 1.1.1 ------------- @@ -628,7 +637,7 @@ cinje has been released under the MIT Open Source license. The MIT License --------------- -Copyright © 2015-2017 Alice Bevan-McGregor and contributors. +Copyright © 2015-2019 Alice Bevan-McGregor and contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the @@ -643,6 +652,7 @@ WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + .. |ghwatch| image:: https://img.shields.io/github/watchers/marrow/cinje.svg?style=social&label=Watch :target: https://github.com/marrow/cinje/subscription :alt: Subscribe to project activity on Github. @@ -683,12 +693,12 @@ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR :target: https://github.com/marrow/cinje/issues :alt: Github Issues -.. |ghsince| image:: https://img.shields.io/github/commits-since/marrow/cinje/1.1.1.svg +.. |ghsince| image:: https://img.shields.io/github/commits-since/marrow/cinje/1.1.2.svg :target: https://github.com/marrow/cinje/commits/develop :alt: Changes since last release. .. |ghtag| image:: https://img.shields.io/github/tag/marrow/cinje.svg - :target: https://github.com/marrow/cinje/tree/1.1.1 + :target: https://github.com/marrow/cinje/tree/1.1.2 :alt: Latest Github tagged release. .. |latestversion| image:: http://img.shields.io/pypi/v/cinje.svg?style=flat diff --git a/cinje/release.py b/cinje/release.py index bd9447e..0b65236 100644 --- a/cinje/release.py +++ b/cinje/release.py @@ -5,7 +5,7 @@ from collections import namedtuple -version_info = namedtuple('version_info', ('major', 'minor', 'micro', 'releaselevel', 'serial'))(1, 1, 1, 'final', 0) +version_info = namedtuple('version_info', ('major', 'minor', 'micro', 'releaselevel', 'serial'))(1, 1, 2, 'final', 0) version = ".".join([str(i) for i in version_info[:3]]) + ((version_info.releaselevel[0] + str(version_info.serial)) if version_info.releaselevel != 'final' else '') author = namedtuple('Author', ['name', 'email'])("Alice Bevan-McGregor", 'alice@gothcandy.com') From 413bdac7242020ce8379d272720c649a9196daa2 Mon Sep 17 00:00:00 2001 From: Alice Bevan-McGregor Date: Thu, 7 Mar 2019 09:45:51 -0500 Subject: [PATCH 06/14] Add additional classifiers to indicate version support. --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 9e0330e..607384e 100755 --- a/setup.py +++ b/setup.py @@ -59,6 +59,8 @@ "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python", From 16ece154da1ea3c4cae19e70b4c6ef083d24c6dc Mon Sep 17 00:00:00 2001 From: Alice Bevan-McGregor Date: Thu, 17 Dec 2020 17:30:57 -0500 Subject: [PATCH 07/14] Import ABCs from the correct location; 3.9 compat. --- cinje/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cinje/util.py b/cinje/util.py index eed2704..88cd883 100644 --- a/cinje/util.py +++ b/cinje/util.py @@ -11,7 +11,8 @@ from codecs import iterencode from inspect import isfunction, isclass from operator import methodcaller -from collections import deque, namedtuple, Sized, Iterable +from collections import deque, namedtuple +from collections.abc import Sized, Iterable from pkg_resources import iter_entry_points from xml.sax.saxutils import quoteattr From cc03344761854bd8351ef6defd2a79d538c49bf8 Mon Sep 17 00:00:00 2001 From: Alice Bevan-McGregor Date: Mon, 16 Aug 2021 15:10:44 -0400 Subject: [PATCH 08/14] Add JSON representation to benchmark suite; mangle some imports to now be Python 3 specific. --- example/benchmark.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/example/benchmark.py b/example/benchmark.py index 4c051f2..2850c1c 100644 --- a/example/benchmark.py +++ b/example/benchmark.py @@ -9,8 +9,7 @@ try: from wheezy.html.utils import escape_html as escape except ImportError: - import cgi - escape = cgi.escape + from html import escape PY3 = sys.version_info[0] >= 3 s = PY3 and str or unicode @@ -328,12 +327,12 @@ def test_chameleon(): else: cheetah_ctx = {} cheetah_template = Template(s("""\ -#import cgi +#import html #for $row in $table #for $key, $value in $row.items - + #end for #end for @@ -355,7 +354,7 @@ def test_cheetah(): test_spitfire = None else: spitfire_template = spitfire.compiler.util.load_template(s("""\ -#from cgi import escape +#from html import escape
$cgi.escape($key)$value$html.escape($key)$value
#for $row in $table @@ -480,6 +479,12 @@ def test_genshi(): return str(result) +import json + +def test_json_dumps(): + return json.dumps(ctx['table']) + + def run(number=100): import profile from timeit import Timer @@ -490,7 +495,7 @@ def run(number=100): print(" msec rps tcalls funcs") for name, test in names: if test: - assert isinstance(test(), s) + assert isinstance(test(), s), "Response is of type: " + repr(type(s)) t = Timer(setup='from __main__ import %s as t' % name, stmt='t()') t = t.timeit(number=number) From ae4b7df599fb6d990d48112beb9472435911e985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Zo=C3=AB=20Bevan=E2=80=93McGregor?= Date: Tue, 29 Nov 2022 14:42:22 -0500 Subject: [PATCH 09/14] Package automation modernization. --- Makefile | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index c1fcce1..0a5863f 100644 --- a/Makefile +++ b/Makefile @@ -3,28 +3,44 @@ USE = development .PHONY: all develop clean veryclean test release -all: clean develop test +all: clean develop test ## Clean caches, refresh project metadata, execute all tests. -develop: ${PROJECT}.egg-info/PKG-INFO +develop: ${PROJECT}.egg-info/PKG-INFO ## Populate project metadata. -clean: +help: ## Show this help message and exit. + @echo "Usage: make \n\033[36m\033[0m" + @awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z_-]+:.*?##/ { printf "\033[36m%-18s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) | sort + +clean: ## Remove executable caches and ephemeral collections. find . -name __pycache__ -exec rm -rfv {} + find . -iname \*.pyc -exec rm -fv {} + find . -iname \*.pyo -exec rm -fv {} + rm -rvf build htmlcov -veryclean: clean - rm -rvf *.egg-info .packaging +veryclean: clean ## Remove all project metadata, executable caches, and sensitive collections. + rm -rvf *.egg-info .packaging/{build,dist,release}/* + +lint: ## Execute pylint across the project. + pylint --rcfile=setup.cfg marrow test: develop - ./setup.py test + pytest + +testloop: ## Automatically execute the test suite limited to one failure. + find marrow test -name \*.py | entr -c pytest --ff --maxfail=1 -q -release: - ./setup.py register sdist bdist_wheel upload ${RELEASE_OPTIONS} - @echo -e "\nView online at: https://pypi.python.org/pypi/${PROJECT} or https://pypi.org/project/${PROJECT}/" - @echo -e "Remember to make a release announcement and upload contents of .packaging/release/ folder as a Release on GitHub.\n" +release: ## Package up and utilize Twine to issue a release. + ./setup.py sdist bdist_wheel ${RELEASE_OPTIONS} + python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/* -${PROJECT}.egg-info/PKG-INFO: setup.py setup.cfg cinje/release.py +${PROJECT}.egg-info/PKG-INFO: setup.py setup.cfg @mkdir -p ${VIRTUAL_ENV}/lib/pip-cache - pip install --cache-dir "${VIRTUAL_ENV}/lib/pip-cache" -Ue ".[${USE}]" + + @# General + @[ ! -e /private ] && pip install --cache-dir "${VIRTUAL_ENV}/lib/pip-cache" -e ".[${USE}]" || true + + @# macOS Specific + @[ -e /private ] && env LDFLAGS="-L/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -L$(brew --prefix openssl@1.1)/lib -L$(brew --prefix)/lib" \ + CFLAGS="-I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include -I$(brew --prefix openssl@1.1)/include -I$(brew --prefix)/include" \ + pip install -e '.[development]' From be19d1083f91c3532ceaedc786a036ee3c54104b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Zo=C3=AB=20Bevan=E2=80=93McGregor?= Date: Tue, 29 Nov 2022 14:42:29 -0500 Subject: [PATCH 10/14] GitHub Actions CI flow. --- .github/workflows/pytest.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/pytest.yml diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..61b2c7e --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,32 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: "Python Test Suite" + +on: + push: + branches: [ develop, master ] + pull_request: + branches: [ develop ] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.8', '3.9', '3.10'] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install -e '.[development]' + - name: Test with pytest + run: | + pytest From 9984f71bb76bc4cb44210eba09e306a04408ed74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Zo=C3=AB=20Bevan=E2=80=93McGregor?= Date: Tue, 29 Nov 2022 14:42:44 -0500 Subject: [PATCH 11/14] Copyright year and version bumps. --- README.rst | 15 ++++++++++++--- cinje/release.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 89d074d..b907ec7 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ cinje ===== - © 2015-2019 Alice Bevan-McGregor and contributors. + © 2015-2022 Alice Bevan-McGregor and contributors. .. @@ -580,6 +580,16 @@ Just like with ``using``, the result of the expression must be a callable genera Version History =============== +Version 1.2.0 +------------- + +* *Fixed* Python 3.9 compatibility by importing ABCs from the correct location. + +* Added Genshi and "raw JSON" to the benchmark suite. + +* Moved test automation from Travis-CI to GitHub Actions. + + Version 1.1.2 ------------- @@ -601,7 +611,6 @@ Version 1.1.1 * *Removed* Python 3.3 testing and support, `flake8` enforcement, and `tox` build/test automation. - Version 1.1 ----------- @@ -637,7 +646,7 @@ cinje has been released under the MIT Open Source license. The MIT License --------------- -Copyright © 2015-2019 Alice Bevan-McGregor and contributors. +Copyright © 2015-2022 Alice Bevan-McGregor and contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the diff --git a/cinje/release.py b/cinje/release.py index 0b65236..53a8769 100644 --- a/cinje/release.py +++ b/cinje/release.py @@ -5,7 +5,7 @@ from collections import namedtuple -version_info = namedtuple('version_info', ('major', 'minor', 'micro', 'releaselevel', 'serial'))(1, 1, 2, 'final', 0) +version_info = namedtuple('version_info', ('major', 'minor', 'micro', 'releaselevel', 'serial'))(1, 2, 0, 'final', 0) version = ".".join([str(i) for i in version_info[:3]]) + ((version_info.releaselevel[0] + str(version_info.serial)) if version_info.releaselevel != 'final' else '') author = namedtuple('Author', ['name', 'email'])("Alice Bevan-McGregor", 'alice@gothcandy.com') From 89ff04d42611b0647fc61e008ad8101ab52a952f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Zo=C3=AB=20Bevan=E2=80=93McGregor?= Date: Tue, 29 Nov 2022 15:36:34 -0500 Subject: [PATCH 12/14] Copyright year. --- LICENSE.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index e98de42..1f6d863 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,8 @@ -Copyright © 2006-2019 Alice Bevan-McGregor and contributors. +Copyright © 2006-2022 Alice Bevan-McGregor and contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + From ae4998c101d55e9813dfff147fa5b065810ab31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Zo=C3=AB=20Bevan=E2=80=93McGregor?= Date: Tue, 29 Nov 2022 15:36:41 -0500 Subject: [PATCH 13/14] Spelling. --- cinje/util.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/cinje/util.py b/cinje/util.py index 88cd883..5576a95 100644 --- a/cinje/util.py +++ b/cinje/util.py @@ -2,25 +2,21 @@ from __future__ import unicode_literals -"""Convienent utilities.""" +"""Convenient utilities.""" # ## Imports import sys from codecs import iterencode -from inspect import isfunction, isclass -from operator import methodcaller from collections import deque, namedtuple from collections.abc import Sized, Iterable +from html.parser import HTMLParser +from inspect import isfunction, isclass +from operator import methodcaller from pkg_resources import iter_entry_points from xml.sax.saxutils import quoteattr -try: # pragma: no cover - from html.parser import HTMLParser -except ImportError: # pragma: no cover - from HTMLParser import HTMLParser - # ## Python Cross-Compatibility # @@ -88,12 +84,12 @@ def flatten(input, file=None, encoding=None, errors='strict'): This has several modes of operation. If no `file` argument is given, output will be returned as a string. The type of string will be determined by the presence of an `encoding`; if one is given the returned value is a - binary string, otherwise the native unicode representation. If a `file` is present, chunks will be written + binary string, otherwise the native Unicode representation. If a `file` is present, chunks will be written iteratively through repeated calls to `file.write()`, and the amount of data (characters or bytes) written returned. The type of string written will be determined by `encoding`, just as the return value is when not writing to a file-like object. The `errors` argument is passed through when encoding. - We can highly recommend using the various stremaing IO containers available in the + We can highly recommend using the various streaming IO containers available in the [`io`](https://docs.python.org/3/library/io.html) module, though [`tempfile`](https://docs.python.org/3/library/tempfile.html) classes are also quite useful. """ @@ -117,7 +113,7 @@ def fragment(string, name="anonymous", **context): **Note:** Use of this function is discouraged everywhere except tests, as no caching is implemented at this time. - Only one function may be declared, either manually, or automatically. If automatic defintition is chosen the + Only one function may be declared, either manually, or automatically. If automatic definition is chosen the resulting function takes no arguments. Additional keyword arguments are passed through as global variables. """ @@ -254,7 +250,7 @@ def xmlargs(_source=None, **values): def chunk(line, mapping={None: 'text', '${': 'escape', '#{': 'bless', '&{': 'args', '%{': 'format', '@{': 'json'}): """Chunkify and "tag" a block of text into plain text and code sections. - The first delimeter is blank to represent text sections, and keep the indexes aligned with the tags. + The first delimiter is blank to represent text sections, and keep the indexes aligned with the tags. Values are yielded in the form (tag, text). """ @@ -522,7 +518,7 @@ def classify(self, line): class Pipe(object): """An object representing a pipe-able callable, optionally with preserved arguments. - Using this you can custruct custom subclasses (define a method named "callable") or use it as a decorator: + Using this you can construct custom subclasses (define a method named "callable") or use it as a decorator: @Pipe def s(text): From 1afec7920a136d6212d6f231669f31e3a0bcce04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Zo=C3=AB=20Bevan=E2=80=93McGregor?= Date: Tue, 29 Nov 2022 15:36:53 -0500 Subject: [PATCH 14/14] Simplification and minor correction. --- setup.py | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/setup.py b/setup.py index 607384e..8324aee 100755 --- a/setup.py +++ b/setup.py @@ -1,17 +1,10 @@ -#!/usr/bin/env python -# encoding: utf-8 - -from __future__ import print_function +#!/usr/bin/env python3 import os import sys import codecs - -try: - from setuptools.core import setup, find_packages -except ImportError: - from setuptools import setup, find_packages +from setuptools import setup if sys.version_info < (2, 7): @@ -52,15 +45,10 @@ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python", @@ -71,7 +59,7 @@ "Topic :: Utilities", ], - packages = find_packages(exclude=['bench', 'docs', 'example', 'test', 'htmlcov']), + packages = ['cinje'], include_package_data = True, package_data = {'': ['README.rst', 'LICENSE.txt']}, namespace_packages = [], @@ -104,6 +92,7 @@ tests_require = tests_require, extras_require = { 'development': tests_require + ['pre-commit'], # Development requirements are the testing requirements. - 'safe': ['webob'], # String safety. + 'safe': ['markupsafe'], # String safety. }, ) +