Objectify — Dotted Navigation¶
pygixml.objectify provides an lxml.objectify-inspired interface that lets you navigate
XML with plain Python attribute access — no .child() or .attribute()
calls needed.
from pygixml import objectify
root = objectify.from_string("""
<database name="users_db" version="1.2">
<user-profile id="101" verified="true">
<first_name>Mohammad</first_name>
<balance>450.75</balance>
</user-profile>
<entry>Value A</entry>
<entry>Value B</entry>
</database>
""")
# Dotted navigation
print(str(root.user_profile.first_name)) # 'Mohammad'
print(root.version) # 1.2 (float)
print(root.user_profile.id) # 101 (int)
print(root.user_profile.verified) # True (bool)
# Text content
print(root.user_profile.balance()) # 450.75 (float)
# Repeated siblings
print([str(e) for e in root.entry]) # ['Value A', 'Value B']
Entry Points¶
- pygixml.objectify.from_string(xml)
Parse an XML string and return the root element as an
ObjectifiedElement.- Parameters:
xml (str) – XML source text.
- Returns:
Document root element.
- Return type:
- Raises:
PygiXMLError – If the XML is malformed.
root = objectify.from_string('<db ver="2"><item>x</item></db>') print(root.ver) # 2 (int) print(str(root.item)) # 'x'
- pygixml.objectify.from_file(path)
Parse an XML file and return the root element as an
ObjectifiedElement.- Parameters:
path (str) – Filesystem path to the XML file.
- Returns:
Document root element.
- Return type:
- Raises:
PygiXMLError – If the file cannot be read or the XML is malformed.
root = objectify.from_file("config.xml") print(root.server.host)
ObjectifiedElement¶
- class pygixml.ObjectifiedElement¶
Wraps a single XML element node and provides attribute-style navigation.
Stores the underlying pugixml
xml_nodestruct directly as a C-level field — no Python wrapper is allocated per access. A_doc_refslot keeps the owningXMLDocumentalive for the lifetime of the wrapper.Navigation
- elem.child_tag
Returns the first child element named
child_tag. If not found, automatically retries with hyphens replacing underscores (elem.user_profile→<user-profile>).When multiple direct siblings share the same tag, a
NodeSequenceis returned instead of a single element.Child elements take priority over same-named attributes.
- elem.attr_name
Returns the type-inferred value of attribute
attr_namewhen no child element with that name exists. Underscores are also mapped to hyphens (elem.data_id→data-id).
- get(name, default=None)¶
Return the value of attribute name, or default if absent. Never raises — behaves like
dict.get(). Only attributes are searched; child elements are not considered.- Parameters:
name (str) – Attribute name (underscores map to hyphens).
default – Returned when the attribute is absent.
- Returns:
Type-inferred attribute value, or default.
root = objectify.from_string('<user id="42" active="true"/>') root.get('id') # 42 root.get('missing') # None root.get('missing', -1) # -1
- find(tag, recursive=True)¶
Return the first descendant element whose tag matches tag, or
Noneif not found. Direct children are checked first; recursion follows when recursive isTrue.- Parameters:
tag (str) – Tag name to search for (underscores map to hyphens).
recursive (bool) – Search all descendants (default
True), or only direct children (False).
- Returns:
First matching element, or
None.- Return type:
pygixml.ObjectifiedElement or None
root = objectify.from_string( '<root><a><b><target>found</target></b></a></root>') root.find('target') # ObjectifiedElement root.find('target', recursive=False) # None
- findall(tag, recursive=True)¶
Return all descendant elements whose tag matches tag, in document order.
- Parameters:
tag (str) – Tag name to search for (underscores map to hyphens).
recursive (bool) – Search all descendants (default
True), or only direct children (False).
- Returns:
All matching elements (may be empty).
- Return type:
root = objectify.from_string(""" <root> <item>a</item> <group><item>b</item></group> <item>c</item> </root>""") root.findall('item') # [a, b, c] root.findall('item', recursive=False) # [a, c]
Text access
- __call__()¶
Return the type-inferred text content of this node (
int,float,bool, orstr). ReturnsNonefor empty or structural nodes.root.user_profile.balance() # 450.75 (float) root.user_profile.first_name() # 'Mohammad'
- __str__()¶
Return the raw text content as a plain
str. Always returns a string — never raises.str(root.user_profile.first_name) # 'Mohammad'
Sequence protocol
- __iter__()¶
Iterate over direct child element nodes.
- __len__()¶
Number of direct child element nodes.
- __bool__()¶
Falseonly for a null (empty) node.
Properties
- tag¶
The XML tag name of this element (
str).
- text_content¶
Raw text content, always a
str.
- attrib¶
All attributes as a
{name: typed_value}dict. Values are type-inferred (bool/int/float/str).root.user_profile.attrib # {'id': 101, 'verified': True}
- xml¶
Serialised XML of this node and its subtree.
NodeSequence¶
- class pygixml.NodeSequence¶
A sequence of
ObjectifiedElementsiblings that share a tag name. Returned by attribute access when multiple direct siblings with the same tag exist.Supports integer indexing (including negative),
len(), and iteration. When exactly one element is present, calling orstr()-ing the sequence delegates to that sole item for convenience.entries = root.entry # NodeSequence (3 items) entries[0] # ObjectifiedElement entries[-1] # last entry [str(e) for e in entries] # ['Value A', 'Value B', 'Value C'] len(entries) # 3
Type Inference¶
Attribute values and leaf-node text are automatically converted to the most specific Python type:
XML value |
Python type |
Example |
|---|---|---|
|
|
|
Integer string |
|
|
Decimal / scientific string |
|
|
Everything else |
|
|
Note
Type inference applies to both attribute values (via elem.attr) and
leaf-node text content (via elem()). str(elem) always returns a
plain str without inference.
Identifier Mapping¶
XML tag names and attribute names often contain hyphens (user-profile,
data-id), which are illegal in Python identifiers. pygixml automatically
maps underscores to hyphens as a fallback:
The exact Python name is tried first (
user_profile→ looks for<user_profile>).If not found, the hyphenated form is tried (
user_profile→<user-profile>).
This means a tag that literally contains an underscore wins over a hyphenated equivalent:
xml = "<root><a_b>underscore</a_b><a-b>hyphen</a-b></root>"
r = objectify.from_string(xml)
str(r.a_b) # 'underscore' ← literal underscore tag wins
Priority Rules¶
When a name exists as both a child element tag and an attribute name, the child element always wins:
xml = '<root name="attr"><name>child</name></root>'
r = objectify.from_string(xml)
r.name # ObjectifiedElement(<name>) ← child wins
r.attrib['name'] # 'attr' ← attribute via .attrib
r.get('name') # 'attr' ← attribute via .get()
Performance Notes¶
ObjectifiedElementandNodeSequencearecdef classobjects compiled intopygixml_cy.so— no pure-Python overhead._nodeholds thexml_nodeC struct directly; no intermediate PythonXMLNodewrapper is allocated per access.Child lookup, attribute lookup, and sibling collection all operate at the C++ level via direct pugixml API calls.
_doc_refis the only Python-level field — it keeps theXMLDocumentalive and prevents premature GC of the underlying pugixml memory pool.
Write Support¶
ObjectifiedElement supports in-place modification via
normal Python assignment and del.
Assignment (elem.name = value)
- elem.child_tag = value
Updates the text content of an existing child element, updates an existing attribute, or creates a new child element — in that priority order.
Values are automatically converted to
strbefore writing.root = objectify.from_string(""" <db version="1.0"> <host>localhost</host> </db> """) # Update existing child element text root.host = "newhost" str(root.host) # 'newhost' # Update existing attribute root.version = 2.0 root.version # 2.0 (float, type-inferred on read) # Create new child element when neither exists root.port = 5432 str(root.port) # '5432'
Priority rules for assignment mirror read priority:
Child element exists → update its text content.
Attribute exists → update the attribute value.
Neither exists → create a new
<name>value</name>child.
When both a child element and an attribute share a name, the child is updated and the attribute is left untouched — consistent with
__getattr__()behaviour.xml = '<root name="attr"><name>child</name></root>' r = objectify.from_string(xml) r.name = "updated" str(r.name) # 'updated' ← child updated r.attrib['name'] # 'attr' ← attribute untouched
Deletion (del elem.name)
- del elem.child_tag
Removes a child element or attribute. Child elements take priority over attributes with the same name. The underscore→hyphen fallback applies.
- Raises AttributeError:
If no matching child or attribute exists.
del root.host # removes <host> element del root.version # removes version="..." attribute del root.user_profile # removes <user-profile> via hyphen mapping