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 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 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]' diff --git a/LICENSE.txt b/LICENSE.txt index 491ef77..1f6d863 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,8 @@ -Copyright © 2006-2018 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. + 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]' diff --git a/README.rst b/README.rst index 03d9070..b907ec7 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ cinje ===== - © 2015-2017 Alice Bevan-McGregor and contributors. + © 2015-2022 Alice Bevan-McGregor and contributors. .. @@ -580,6 +580,25 @@ 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 +------------- + +* *Fixed* `Python 3.7 exception use within generators. `_ + +* *Added* Genshi to the `benchmark comparison suite `_. + +* *Fixed* minor docstring typo. + Version 1.1.1 ------------- @@ -592,7 +611,6 @@ Version 1.1.1 * *Removed* Python 3.3 testing and support, `flake8` enforcement, and `tox` build/test automation. - Version 1.1 ----------- @@ -628,7 +646,7 @@ cinje has been released under the MIT Open Source license. The MIT License --------------- -Copyright © 2015-2017 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 @@ -643,6 +661,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 +702,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/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(' ') diff --git a/cinje/release.py b/cinje/release.py index bd9447e..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, 1, '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') diff --git a/cinje/util.py b/cinje/util.py index eed2704..5576a95 100644 --- a/cinje/util.py +++ b/cinje/util.py @@ -2,24 +2,21 @@ from __future__ import unicode_literals -"""Convienent utilities.""" +"""Convenient utilities.""" # ## Imports import sys from codecs import iterencode +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 collections import deque, namedtuple, Sized, Iterable 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 # @@ -87,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. """ @@ -116,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. """ @@ -253,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). """ @@ -521,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): diff --git a/example/benchmark.py b/example/benchmark.py index cfd9ad3..2850c1c 100644 --- a/example/benchmark.py +++ b/example/benchmark.py @@ -7,482 +7,507 @@ 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 + from html import 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("""\ -#import cgi + cheetah_ctx = {} + cheetah_template = Template(s("""\ +#import html - #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
$html.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("""\ -#from cgi import escape + spitfire_template = spitfire.compiler.util.load_template(s("""\ +#from html 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) + + +import json + +def test_json_dumps(): + return json.dumps(ctx['table']) 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), "Response is of type: " + repr(type(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() diff --git a/setup.py b/setup.py index 9e0330e..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,13 +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.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python", @@ -69,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 = [], @@ -102,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. }, ) +