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/>"]
["$text" "<foo>"]

These will be rendered as:

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

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 height="100" width="200" 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. See the example below.

from mu import Node, xml

class UL(Node):
    def __init__(self, **attrs):
        super().__init__("ul", **attrs)

    def __call__(self, *nodes, **attrs):
        nodes = [["li", node] for node in nodes]
        return super().__call__(*nodes, **attrs)

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

xml(["div", UL(), "foo"])
<div><ul/>foo</div>

Here the UL() 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", [UL(), {"class": ("foo", "bar")}, "item 1", "item 2", "item 3"]])
<div>
  <ul class="foo bar">
    <li>item 1</li>
    <li>item 2</li>
    <li>item 3</li>
  </ul>
</div>

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

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

Note that we cannot use the reserved class keyword, instead use cls to get a class attribute.

<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"]]]

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.

uvx ruff check

Run formatter.

uvx ruff format

Run tests.

uv run pytest

Or with coverage and missing lines.

uv run pytest --cov-report term-missing --cov=mu

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.3.tar.gz (29.3 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.3-py3-none-any.whl (9.6 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for mu_xml-0.1.3.tar.gz
Algorithm Hash digest
SHA256 cecb0f81175a962fe48b75e7cf92b147a4381aed90af6516365a4972754f8af8
MD5 7f438d6ce579ac6a1a37d4c33da45a41
BLAKE2b-256 98a188cc3261420a1488ce93ea1449cdf3b271b28b551e6f96c35338a965fcc1

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for mu_xml-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 92639696758ddd0eee9576004ad386aed58df374d9a6911e1670e9e55fa99751
MD5 2c63e28125e5c90c8b533f13d3464aac
BLAKE2b-256 21306fcdfdcbcbbffdb5fe336ef79f6b4b303d1f95c6f5aa7a1879bc8569aa5a

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