Skip to main content

Represent HTML and XML using Python data structures.

Project description

Mu-XML

Represent XML using Python data structures. This does for Python what the Hiccup library by James Reeves did for the Clojure language.

Warning: this library is still alpha. So expect breaking changes.

Install

pip install mu-xml
# or
uv add mu-xml

Usage

To render a Mu data structure as XML markup use the xml function.

from mu import xml

xml(["p", "Hello, ", ["b", "World"], "!"])

Returns the string <p>Hello, <b>World</b>!</p>

Note that serializing to a string will not guarantee well-formed XML.

Documentation

XML is a tree data structure made up of various node types such as element, attribute, or text nodes.

However, writing markup in code is tedious and error-prone. Mu allows creating markup with Python code and basic Python data structures.

Element nodes

An element node is made up of a tag, an optional attribute dictionary and zero or more content nodes which themselves can be made up of other elements.

el = ["p", {"id": 1}, "this is a paragraph."]

You can access the individual parts of an element node using the following accessor functions.

import mu

mu.tag(el)            # "p"
mu.attrs(el)          # {"id": 1}
mu.content(el)        # ["this is a paragraph."]
mu.get_attr("id", el) # 1

To render this as XML markup:

from mu import xml

xml(el)    # <p id="1">this is a paragraph.</p>

Use the provided predicate functions to inspect a node.

import mu

mu.is_element(el)       # is this a valid element node?
mu.is_special_node(el)  # is this a special node? (see below)
mu.is_empty(el)         # does it have child nodes?
mu.has_attrs(el)        # does it have attributes?

Special nodes

XML has a few syntactic constructs that you usually don't need. But if you do need them, you can represent them in Mu as follows.

["$comment", "this is a comment"]
["$pi", "foo", "bar"]
["$cdata", "<foo>"]
["$raw", "<foo/>"]

These will be rendered as:

<!-- this is a comment -->
<?foo bar?>
&lt;foo&gt;
<foo/>

Nodes with tag names that start with $ are reserved for other applications. The xml() function will drop special nodes that it does not recognize.

A $cdata node will not escape it's content as is usual in XML and HTML. A $raw node is very useful for adding string content that already contains markup.

A $comment node will ensure that the forbidden -- is not part of the comment text.

Namespaces

Mu does not enforce XML rules. You can use namespaces but you have to provide the namespace declarations as is expected by XML Namespaces.

["svg", dict(xmlns="http://www.w3.org/2000/svg"),
  ["rect", dict(width=200, height=100, x=10, y=10)]
]
<svg xmlns="http://www.w3.org/2000/svg">
  <rect width="200" height="100" x="10" y="10"/>
</svg>

The following uses explicit namespace prefixes and is semantically identical to the previous example.

["svg:svg", {"xmlns:svg": "http://www.w3.org/2000/svg"},
  ["svg:rect", {"width": 200, "height": 100, "x": 10, "y": 10}]
]
<svg:svg xmlns:svg="http://www.w3.org/2000/svg">
  <svg:rect widht="200" height="100" x="10" y="10"/>
</svg:svg>

Object nodes

Object nodes may appear in two positions inside a Mu data structure.

  1. In the content position of an element node (e.g. ["p", {"class": "x"}, obj]) or,
  2. In the tag position of an element node (e.g. [obj, {"class": "x"}, "content"])

Object nodes can be derived from the mu.Node class and must implement the mu method which should generate well-formed Mu data. This method will be called when rendering or expanding the Mu data structure.

As an example take the following custom class definition.

import mu
from mu import xml

class OL(mu.Node):

    def mu(self):
        ol = ["ol"]
        if len(self.attrs) > 0:
            ol.append(self.attrs)
        for item in self.content:
            ol.append(["li", item])
        return ol

Let's use this class in a Mu data structure.

xml(["div", OL(), "foo"])
<div><ol/>foo</div>

Here the OL() object is in the content position so no information is passed to it to render a list. This may not be what you wanted to achieve.

To produce a list the object must be in the tag position of an element node.

xml(["div", [OL(), {"class": ("foo", "bar")}, "item 1", "item 2", "item 3"]])
<div>
  <ol class="foo bar">
    <li>item 1</li>
    <li>item 2</li>
    <li>item 3</li>
  </ol>
</div>

You can also provide some initial content and attributes in the object node constructor.

xml(["div", [OL("item 1", id=1, cls=("foo", "bar")), "item 2", "item 3"]])

Note that we cannot use the reserved class keyword, instead use cls to get a class attribute. It is a bit of a hack.

<div>
  <ol class="foo bar" id="1">
    <li>item 1</li>
    <li>item 2</li>
    <li>item 3</li>
  </ol>
</div>

Expand nodes

In some cases you may want to use the mu.expand function to only expand object nodes to a straightforward data structure.

from mu import expand

expand(["div", [OL(), {"class": ("foo", "bar")}, "item 1", "item 2", "item 3"]])
["div",
  ["ol", {"class": ("foo", "bar")},
    ["li", "item 1"],
    ["li", "item 2"],
    ["li", "item 3"]]]

Apply nodes

A third and final method of building a document is mu.apply. It gets a dictionary with rules. The values of the dictionary are either a replacement value or a mu.Node (or something that looks like one).

Using the previous example of the UL object we can illustrate how my.apply works.

Say we have a Mu data structure in which we want to replace each foo element with an unordered list node object.

from mu import apply

apply(
  ["doc", ["foo", {"class": "x"}, "item 1", "item 2"]],
  {"foo": OL()})
["doc",
  ["ol", {"class": "x"}, ["li", "item 1"], ["li", "item 2"]]]

You can also pass in literal values that get replaced when the element name matches a rule.

apply(
  ["doc", ["$foo"], ["bar"], ["$foo"]],
  {"$foo": ["BAR"]})
["doc",
  ["BAR"],["bar"], ["BAR"]]

Note that when object nodes are found they won't get expanded unless they are present in the rules dictionary.

Mu documents as YAML

Many Mu documents can be expressed as YAML quite elegantly.

[foo,[bar, {a: 1, class: bla}, just saying]]

Let's try something more complicated (a Docbook cooking recipe).

[
    [sect1,
        [title, What do you need?],
        [sect2,
            [title, Ingredients],
            [para]],
        [sect2,
            [title, Equipment],
            [para]]],
    [sect1, {id: sec.preparation},
        [title, Preparation],
        [itemizedlist,
            [listitem,
                [para, 60g Habanero Chilis]],
            [listitem,
                [para, 30g Cayenne Chilis]],
            [listitem,
                [para, "1,5 Butch T Chilis"]],
            [listitem,
                [para, 75g Kidney Beans]]]]
]

Etc. you get the idea. It's not ideal (e.g. some characters, such as commas, may require text to be quoted) and using coding we can make such documents using code.

Mu documents as code

Use code to construct larger documents.

# see examples/docbook-cooking.py

Serializing Python data structures

mu.dumps(["a",True,3.0])
mu.loads(['_', {'as': 'array'},
  ['_', 'a'],
  ['_', {'as': 'boolean', 'value': 'true()'}],
  ['_', {'as': 'float'}, 3.0]])
mu.dumps(dict(a="a",b=True,c=3.0))
mu.loads(['_', {'as': 'object'},
  ['a', 'a'],
  ['b', {'as': 'boolean', 'value': 'true()'}],
  ['c', {'as': 'float'}, 3.0]])

When dumps() encounters a Python object it will call it's mu() method if it exists otherwise it will not be part of the serialized result. A function object will be called and it's return value becomes part of the serialized result.

Develop

  • Install uv.
  • uv tool add ruff
  • Maybe install Ruff VS Code extension

Run linter.

ruff check mu

Run formatter.

ruff format mu

Run tests.

uv tool run pytest
# or
uvx pytest

Or with coverage.

uv tool run pytest --cov-report term --cov=mu
uvx pytest --cov-report term --cov=mu

Publish

uv version 0.1.2
uv build
git push origin
git checkout main
git merge dev

uv publish

Related work

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mu_xml-0.1.2.tar.gz (29.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mu_xml-0.1.2-py3-none-any.whl (10.1 kB view details)

Uploaded Python 3

File details

Details for the file mu_xml-0.1.2.tar.gz.

File metadata

  • Download URL: mu_xml-0.1.2.tar.gz
  • Upload date:
  • Size: 29.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.9

File hashes

Hashes for mu_xml-0.1.2.tar.gz
Algorithm Hash digest
SHA256 cb6bdbcec66d095f4693444930793e4f94903ce02a901822963093b62c69f729
MD5 c18804068958dfa26331a796eba0264b
BLAKE2b-256 da6b66a94acb00c35366f19b04b4bcd168e8145e5b763777f0c90fe86877ed44

See more details on using hashes here.

File details

Details for the file mu_xml-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: mu_xml-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 10.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.9

File hashes

Hashes for mu_xml-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 171a36dec80ade37d1e46c0bfc3d9ecc46dd3d4ed3f174df2db0da79e217b0cc
MD5 e6304c4943d20ffa24d429b30c7aafc3
BLAKE2b-256 05494f894191d9ba6287379a4e5ed4db6a19b88375108bf55c475a1b33b8edbf

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page