From 7b0f423908eaa0ea7b959720cb6a732586a2e441 Mon Sep 17 00:00:00 2001 From: Yann Gravrand Date: Thu, 19 May 2016 21:16:14 +0200 Subject: [PATCH 1/5] Renderer additions (WIP) + CI --- examples/app1.py | 4 +- katla/renderer.py | 146 +++++++++++++++++++++++-- katla/tag.py | 14 ++- katla/{attributes.py => transforms.py} | 3 + setup.py | 3 +- tests/test_component.py | 6 +- tests/test_tag.py | 6 +- travis.yml | 12 ++ 8 files changed, 176 insertions(+), 18 deletions(-) rename katla/{attributes.py => transforms.py} (80%) create mode 100644 travis.yml diff --git a/examples/app1.py b/examples/app1.py index 74afeac..f61e14f 100644 --- a/examples/app1.py +++ b/examples/app1.py @@ -7,7 +7,7 @@ from werkzeug.wrappers import Request, Response -from katla.renderer import HTML5Renderer +from katla.renderer import HTMLRenderer from app1_root import Root @@ -18,7 +18,7 @@ def __init__(self): self.root = Root() def dispatch_request(self, request): - res = HTML5Renderer().render(self.root) + res = HTMLRenderer().render(self.root) return Response(res.to_html_string(), mimetype='text/html') def wsgi_app(self, environ, start_response): diff --git a/katla/renderer.py b/katla/renderer.py index a3f8c81..eab8fe4 100644 --- a/katla/renderer.py +++ b/katla/renderer.py @@ -7,7 +7,7 @@ from lxml import etree -from . import attributes +from . import transforms from .tag import tag @@ -25,8 +25,8 @@ def to_html_string(self): doctype=None) -base_attributes = {'style': attributes.style, - 'className' : attributes.className} +base_transforms = {'style': transforms.style, + 'className' : transforms.className} class BaseRenderer(object): @@ -37,8 +37,140 @@ def render(self, comp): return RenderedTree(tree) -class HTML5Renderer(BaseRenderer): +class HTMLRenderer(BaseRenderer): - # div = partial(etree.Element, 'div') - span = tag('span', base_attributes) - div = tag('div', base_attributes) + # From list @ https://developer.mozilla.org/fr/docs/Web/HTML/Element + + html = tag('html') + + # Meta + base = tag('base', base_transforms) + head = tag('head', base_transforms) + link = tag('link', base_transforms) + meta = tag('meta', base_transforms) + style = tag('style', base_transforms) + title = tag('title', base_transforms) + + # Content + address = tag('address', base_transforms) + article = tag('article', base_transforms) + body = tag('body', base_transforms) + footer = tag('footer', base_transforms) + header = tag('header', base_transforms) + h1 = tag('h1', base_transforms) + h2 = tag('h2', base_transforms) + h3 = tag('h3', base_transforms) + h4 = tag('h4', base_transforms) + h5 = tag('h5', base_transforms) + h6 = tag('h6', base_transforms) + hgroup = tag('hgroup', base_transforms) + nav = tag('nav', base_transforms) + section = tag('section', base_transforms) + + # Text content + blockquote = tag('blockquote', base_transforms) + dd = tag('dd', base_transforms) + div = tag('div', base_transforms) + dl = tag('dl', base_transforms) + dt = tag('dt', base_transforms) + figcaption = tag('figcaption', base_transforms) + figure = tag('figure', base_transforms) + hr = tag('hr', base_transforms) + li = tag('li', base_transforms) + main = tag('main', base_transforms) + ol = tag('ol', base_transforms) + p = tag('p', base_transforms) + pre = tag('pre', base_transforms) + ul = tag('ul', base_transforms) + + # Text semantics + a = tag('a', base_transforms) + abbr = tag('abbr', base_transforms) + b = tag('b', base_transforms) + bdi = tag('bdi', base_transforms) + bdo = tag('bdo', base_transforms) + br = tag('br', base_transforms) + cite = tag('cite', base_transforms) + code = tag('code', base_transforms) + data = tag('data', base_transforms) + dfn = tag('dfn', base_transforms) + em = tag('em', base_transforms) + i = tag('i', base_transforms) + kbd = tag('kbd', base_transforms) + mark = tag('mark', base_transforms) + q = tag('q', base_transforms) + rp = tag('rp', base_transforms) + rt = tag('rt', base_transforms) + ruby = tag('ruby', base_transforms) + s = tag('s', base_transforms) + samp = tag('samp', base_transforms) + small = tag('small', base_transforms) + span = tag('span', base_transforms) + strong = tag('strong', base_transforms) + sub = tag('sub', base_transforms) + sup = tag('sup', base_transforms) + time = tag('time', base_transforms) + u = tag('u', base_transforms) + var = tag('var', base_transforms) + wbr = tag('wbr', base_transforms) + + # Image and multimedia + area = tag('area', base_transforms) + audio = tag('audio', base_transforms) + img = tag('img', base_transforms) + map_ = tag('map', base_transforms) + track = tag('track', base_transforms) + video = tag('video', base_transforms) + + # Embedded content + embed = tag('embed', base_transforms) + iframe = tag('iframe', base_transforms) + object = tag('object', base_transforms) + param = tag('param', base_transforms) + source = tag('source', base_transforms) + + # Scripts + canvas = tag('canvas', base_transforms) + script = tag('script', base_transforms) + noscript = tag('noscript', base_transforms) + + # Edition + del_ = tag('del', base_transforms) + ins = tag('ins', base_transforms) + + # Tables + caption = tag('caption', base_transforms) + col = tag('col', base_transforms) + colgroup = tag('colgroup', base_transforms) + table = tag('table', base_transforms) + tbody = tag('tbody', base_transforms) + td = tag('td', base_transforms) + tfoot = tag('tfoot', base_transforms) + th = tag('th', base_transforms) + thead = tag('thead', base_transforms) + tr = tag('tr', base_transforms) + + # Forms + button = tag('button', base_transforms) + datalist = tag('datalist', base_transforms) + colgroup = tag('colgroup', base_transforms) + fieldset = tag('fieldset', base_transforms) + form = tag('form', base_transforms) + input_ = tag('input', base_transforms) + keygen = tag('keygen', base_transforms) + label = tag('label', base_transforms) + legend = tag('legend', base_transforms) + meter = tag('meter', base_transforms) + optgroup = tag('optgroup', base_transforms) + option = tag('option', base_transforms) + output = tag('output', base_transforms) + progress = tag('progress', base_transforms) + select = tag('select', base_transforms) + textarea = tag('textarea', base_transforms) + + # Interactive + details = tag('details', base_transforms) + dialog = tag('dialog', base_transforms) + menu = tag('menu', base_transforms) + menuitem = tag('menuitem', base_transforms) + summary = tag('summary', base_transforms) diff --git a/katla/tag.py b/katla/tag.py index e414840..1d3b24d 100644 --- a/katla/tag.py +++ b/katla/tag.py @@ -11,8 +11,20 @@ class Tag(object): + """Tag is the base class to render HTML tags""" def __init__(self, name, attrs_transforms): + """Init a tag. + :param name: Name of the tag to use in output + :param attrs_transforms: either: + - a dictionary of {``attribute_name``:``attribute_transform``}, + each ``attribute_transform`` being a function: + :param name: Original attribute name + :param value: Original attribute value + :return: A (``name``, ``value``) tuple with the new attribute name and value + - a list of allowed attribute names + - None to apply no transform and disable restriction on attribute names + """ self._name = name self._attrs_transforms = None if attrs_transforms is None: @@ -44,7 +56,6 @@ def _extend(self, tree, children): self._set_text(tree, child) # TODO comment case elif isinstance(child, Component): - print 'COMP CASE' res = child.render(self._renderer) if res is not None: tree.append(res) @@ -66,4 +77,5 @@ def _handle_attrs(self, attrs): def tag(name, attrs_transforms=None): + """Creates a ``Tag``, see ``Tag:__init__`` for documentation""" return Tag(name, attrs_transforms) diff --git a/katla/attributes.py b/katla/transforms.py similarity index 80% rename from katla/attributes.py rename to katla/transforms.py index a8afbfe..60b9548 100644 --- a/katla/attributes.py +++ b/katla/transforms.py @@ -3,6 +3,9 @@ katla :copyright: (c) 2016 by Yann Gravrand. :license: BSD, see LICENSE for more details. + + The ``transforms`` module defines usual transforms on attributes, + e.g. "className" --> "class" """ def className(name, val): diff --git a/setup.py b/setup.py index 9f05303..76ae2d9 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,7 @@ packages=find_packages(), zip_safe=True, install_requires=( - 'werkzeug', 'lxml', ), - extras_require={'test': ('pytest',)} + extras_require={'test': ('pytest', 'werkzeug')} ) diff --git a/tests/test_component.py b/tests/test_component.py index 1f5a16e..493aa13 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -7,9 +7,9 @@ import unittest -from katla import attributes +from katla import transforms from katla.component import Component -from katla.renderer import BaseRenderer, base_attributes +from katla.renderer import BaseRenderer, base_transforms from katla.tag import tag from .utils import render_tag @@ -29,7 +29,7 @@ def render(self, r): class Renderer(BaseRenderer): - div = tag('div', base_attributes) + div = tag('div', base_transforms) class TestComponent(unittest.TestCase): diff --git a/tests/test_tag.py b/tests/test_tag.py index 3d1b38c..3223c33 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -7,7 +7,7 @@ import unittest -from katla import attributes +from katla import transforms from katla.tag import tag from .utils import render_tag @@ -17,8 +17,8 @@ class TestTag(unittest.TestCase): div = tag('div') span = tag('span', ('alt',)) - a = tag('a', {'className': attributes.className, - 'style': attributes.style}) + a = tag('a', {'className': transforms.className, + 'style': transforms.style}) def test_tags_and_text(self): d = self.div() diff --git a/travis.yml b/travis.yml new file mode 100644 index 0000000..2590558 --- /dev/null +++ b/travis.yml @@ -0,0 +1,12 @@ +language: python +python: + - "2.6" + - "2.7" + - "3.2" + - "3.3" + - "3.4" + - "3.5" + - "pypy" + +install: "pip install -e ." +script: py.test From 2141713809f3a736ffc9a77312ef1707d3f3e7d6 Mon Sep 17 00:00:00 2001 From: Yann Gravrand Date: Thu, 19 May 2016 21:19:35 +0200 Subject: [PATCH 2/5] Typo in CI config filename --- travis.yml => .travis.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename travis.yml => .travis.yml (100%) diff --git a/travis.yml b/.travis.yml similarity index 100% rename from travis.yml rename to .travis.yml From a30b029046aaa85c76187b585a208701516ac729 Mon Sep 17 00:00:00 2001 From: Yann Gravrand Date: Thu, 19 May 2016 21:23:18 +0200 Subject: [PATCH 3/5] Py3 fix --- katla/tag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/katla/tag.py b/katla/tag.py index 1d3b24d..cfaee76 100644 --- a/katla/tag.py +++ b/katla/tag.py @@ -7,7 +7,7 @@ from lxml import etree -from component import Component +from .component import Component class Tag(object): From 041bcbf978e35c7cc2b9dfa1f65606d47ac89f61 Mon Sep 17 00:00:00 2001 From: Yann Gravrand Date: Fri, 20 May 2016 12:27:32 +0200 Subject: [PATCH 4/5] Py > 3.2 support --- .travis.yml | 3 +-- katla/tag.py | 7 +++++++ tests/test_component.py | 4 ++-- tests/test_tag.py | 20 ++++++++++---------- tests/utils.py | 2 +- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2590558..54681b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,10 @@ language: python python: - "2.6" - "2.7" - - "3.2" - "3.3" - "3.4" - "3.5" - "pypy" -install: "pip install -e ." +install: "pip install -e .[test]" script: py.test diff --git a/katla/tag.py b/katla/tag.py index cfaee76..73d1cc9 100644 --- a/katla/tag.py +++ b/katla/tag.py @@ -4,6 +4,13 @@ :copyright: (c) 2016 by Yann Gravrand. :license: BSD, see LICENSE for more details. """ +# Python 3 handling +try: + unicode = unicode +except NameError: + str, unicode, bytes, basestring = str, str, bytes, (str, bytes) +else: + str, unicode, bytes, basestring = str, unicode, str, basestring from lxml import etree diff --git a/tests/test_component.py b/tests/test_component.py index 493aa13..0784a2e 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -38,12 +38,12 @@ def test_simple(self): a = A() r = Renderer() self.assertEquals(render_tag(a.render(r)), - '
Hello World
', + b'
Hello World
', 'Simple component rendering') def test_composition(self): b = B() r = Renderer() self.assertEquals(render_tag(b.render(r)), - '
Wrapped:
Hello World
Hello World
', + b'
Wrapped:
Hello World
Hello World
', 'Simple component rendering') diff --git a/tests/test_tag.py b/tests/test_tag.py index 3223c33..582d0ff 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -22,49 +22,49 @@ class TestTag(unittest.TestCase): def test_tags_and_text(self): d = self.div() - self.assertEquals(render_tag(d), '
', + self.assertEquals(render_tag(d), b'
', 'Empty tag') d = self.div('Hello') - self.assertEquals(render_tag(d), '
Hello
', + self.assertEquals(render_tag(d), b'
Hello
', 'Text only tag') d = self.div('Hello', ' ', 'World') - self.assertEquals(render_tag(d), '
Hello World
', + self.assertEquals(render_tag(d), b'
Hello World
', 'Tag with multiple concatenated texts') d = self.div('Hello', self.div('World')) - self.assertEquals(render_tag(d), '
Hello
World
', + self.assertEquals(render_tag(d), b'
Hello
World
', 'Child tag') d = self.div('Hello', self.span(' World'), ' ') - self.assertEquals(render_tag(d), '
Hello World
', + self.assertEquals(render_tag(d), b'
Hello World
', 'Child tag and texts') def test_tag_attributes(self): d = self.div('Hello', ' ', 'World', title='Hello', alt='Hello') self.assertEquals(render_tag(d), - '
Hello World
', + b'
Hello World
', 'Multiple attributes') d = self.div('Hello', self.span('World', alt='World'), ' ', title="Hello") self.assertEquals(render_tag(d), - '
HelloWorld
', + b'
HelloWorld
', 'Child with attributes') d = self.span('Hi', alt='World', title='World') - self.assertEquals(render_tag(d), 'Hi', + self.assertEquals(render_tag(d), b'Hi', 'Forbidden attributes are cleared') d = self.a('Link', className='link') self.assertEquals(render_tag(d), - 'Link', + b'Link', 'Attribute transform: className example') d = self.a('Link', style={'display': 'none', 'text-decoration': 'underline'}) self.assertEquals(render_tag(d), - 'Link', + b'Link', 'Attribute transform: style example') diff --git a/tests/utils.py b/tests/utils.py index 66a3ec9..c3896e4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -15,5 +15,5 @@ def render_tag(tag): pretty_print=False, xml_declaration=False, doctype=None) - print res + print(res) return res From a6e500e764b51a5d7068b4e3356c4199fb0f66b6 Mon Sep 17 00:00:00 2001 From: Yann Gravrand Date: Sat, 4 Jun 2016 13:24:16 +0200 Subject: [PATCH 5/5] Cleaner styles system and tests --- katla/styles.py | 24 ++++++++++++++++++++++++ katla/tag.py | 11 ++++++----- katla/transforms.py | 6 +++--- tests/test_tag.py | 9 +++++---- 4 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 katla/styles.py diff --git a/katla/styles.py b/katla/styles.py new file mode 100644 index 0000000..1e6ef8a --- /dev/null +++ b/katla/styles.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +""" + katla + :copyright: (c) 2016 by Yann Gravrand. + :license: BSD, see LICENSE for more details. +""" + + +def from_string(s): + """Returns a dictionary of styles from a styles string + :param s: Input string, eg. "display:none; text-align: right" + :return: A dictionary of styles {declaration: value} + """ + declarations = [decl.strip().split(':') for decl in s.split(';')] + return {name.strip(): value.strip() for (name, value) in declarations} + + +def to_string(d): + """Returns a string from a dictionary of styles + :param s: Styles dictionary, eg. {"display": "none", "text-align": "right"} + :return: A styles string + """ + return '; '.join(['%s: %s' % + (name, value) for name, value in d.items()]) diff --git a/katla/tag.py b/katla/tag.py index 73d1cc9..709b5bb 100644 --- a/katla/tag.py +++ b/katla/tag.py @@ -4,6 +4,11 @@ :copyright: (c) 2016 by Yann Gravrand. :license: BSD, see LICENSE for more details. """ +from lxml import etree + +from .component import Component + + # Python 3 handling try: unicode = unicode @@ -12,13 +17,9 @@ else: str, unicode, bytes, basestring = str, unicode, str, basestring -from lxml import etree - -from .component import Component - class Tag(object): - """Tag is the base class to render HTML tags""" + """``Tag`` is the base class to render HTML tags""" def __init__(self, name, attrs_transforms): """Init a tag. diff --git a/katla/transforms.py b/katla/transforms.py index 60b9548..9f038ae 100644 --- a/katla/transforms.py +++ b/katla/transforms.py @@ -8,13 +8,13 @@ e.g. "className" --> "class" """ +from . import styles + def className(name, val): return 'class', val def style(name, val): if isinstance(val, dict): - return (name, '; '.join(['%s: %s' % - (attr_name, attr_value) for \ - attr_name, attr_value in val.items()])) + return name, styles.to_string(val) return name, val diff --git a/tests/test_tag.py b/tests/test_tag.py index 582d0ff..b387854 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -7,7 +7,7 @@ import unittest -from katla import transforms +from katla import styles, transforms from katla.tag import tag from .utils import render_tag @@ -61,10 +61,11 @@ def test_tag_attributes(self): b'Link', 'Attribute transform: className example') - d = self.a('Link', style={'display': 'none', - 'text-decoration': 'underline'}) + st = {'display': 'none', + 'text-decoration': 'underline'} + d = self.a('Link', style=st) self.assertEquals(render_tag(d), - b'Link', + b'Link' % styles.to_string(st), 'Attribute transform: style example')