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

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

Whe can write code to help with certain structures. For example the list can be generated by an object node.

# 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.

Run tests.

pytest tests/

Or with coverage.

pytest --cov-report term --cov=mu tests/

Run mypy type checking.

pip install mypy
mypy --config-file pyproject.toml ./

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.1.tar.gz (33.1 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.1-py3-none-any.whl (8.7 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for mu_xml-0.1.1.tar.gz
Algorithm Hash digest
SHA256 0c6c3d3e9a42a8467af9fa3ad20bca560c5baa9414e870f22ce47175160469dc
MD5 36c5ecce85154978dd5791f1874ee684
BLAKE2b-256 48cec0d0b08ceb860245e2e087145e19c68a2bfa343b9a0a568d3d31e1a0162f

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for mu_xml-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c759c44f8cfbd69f4d14ac7c92c0073285cefcd3e56e7db520e36b5351c2c9f9
MD5 e891e3b43adb68aede50537d76942d49
BLAKE2b-256 fb8707d44c206876687501ae10c7bbe6dc5a94555196e1ce75a294fd55a96bef

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