2
\$\begingroup\$

I have written this little tagging module to make it easier to generate small html snippets on the fly. The examples below show how this make things a bit easier. My question is about the handling of tags that contain either a single element like text or a list of other elements, like the tr element in the second example. At the moment, the code has to jump though some unpythonic loops to handle the different cases. How do I write code that handles either a single thing or a list of those things with one function in an elegant way?

""" Supplies some utilities for creating html documents manually

>>> print(h1('test'))
<h1>
test
</h1>

>>> print(table([tr([td('d1'), td('d2')]), tr()]))
<table>
<tr>
<td>
d1
</td>
<td>
d2
</td>
</tr>
<tr />
</table>
"""
from __future__ import print_function

class tag:
    """ represents an html tag - specifically used to create outputs for web pages

    Basic usage - create a "test" tag:

    >>> print(tag('test'))
    <test />

    Attributes can be specified as a dictionary

    >>> print(tag('test', attributes={'prop': 'value'}))
    <test prop='value' />

    The real benefit - tags can be nested, allowing the python interpreter to track closing tags:

    >>> print(tag('test', tag('inner')))
    <test>
    <inner />
    </test>
    """
    def __init__(self, name, contents=[], attributes={}):
        self.name = name
        self.contents = contents
        self.attributes = attributes
    def __str__(self):
        tagcontents = self.name
        if len(self.attributes) > 0:
            attrstring = " ".join("%s='%s'" % kv for kv in self.attributes.items())
            tagcontents += " " + attrstring

        if self.contents:
            if hasattr(self.contents, '__iter__') and not hasattr(self.contents, 'strip'):
                contentstring = "\n".join(str(i) for i in self.contents)
            else:
                contentstring = str(self.contents)
            return "<%s>\n%s\n</%s>" % (tagcontents, contentstring, self.name)
        else:
            return "<" + tagcontents + " />"
    def __repr__(self):
        return "tag(%s, contents=%s, attributes=%s)" % (self.name, self.contents, self.attributes)

def namedtag(t):
    """ create a named tag class - useful for standard html tags

    >>> mytag = namedtag('mytag')
    >>> print(mytag())
    <mytag />

    """
    class n(tag):
        def __init__(self, contents=[], attributes={}):
            tag.__init__(self, t, contents, attributes)
    return n

h1, h2, p, table, td, th, tr, a = (namedtag(t) for t in ("h1", "h2", "p", "table", "td", "th", "tr", "a"))
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

The solution is to have contents always be a list. If we don't want to impose this onto the caller of the constructor, we have to use a variable number of arguments:

def __init__(self, name, *contents, **attrs):
   ...

Now the functions can be called as table(tr(td('d1'), td('d2')), tr()) or more generally:

foo("name", child1, child2, ... attr1=value, attr2=value, ...)

Then inside the function, contents will be a tuple and attrs a dictionary.


Otherwise, I find your inappropriate usage of OOP unnecessary. For example, the class n does not really add value, and violates the Liskov Substitution Principle – the constructor signature is incompatible with the parent class. Instead, use closures to provide a default argument:

def namedtag(name):
    return lambda *contents, **kws: tag(name, *contents, **kws)
\$\endgroup\$
2
  • \$\begingroup\$ I really like this solution - it's compact and makes a lot of sense from the caller point of view. I also agree about the namedtag class. I think I'm going to replace it using partial. \$\endgroup\$ Commented Jan 15, 2014 at 9:17
  • \$\begingroup\$ If you really do want to create subclasses of tag, you could try spelling it def namedtag(name): return type(name, (tag,), dict()) and changing tag.__init__'s use of name accordingly. But unless you are going to leverage inheritance somehow, I'd stick with the suggestion of a factory function. \$\endgroup\$ Commented Jan 16, 2014 at 21:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.