From 24861ffac776f3af18fc751f1e913ad1747d4d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Mon, 2 Mar 2026 22:58:44 +0100 Subject: [PATCH 1/7] WIP: switch XML API to ElementTree bridge Replace Python libxml2 XML objects with ElementTree wrappers and make the C extension return serialized XML bytes. Update tests/examples and drop xml2mod linkage and packaging deps as the next step toward removing Python libxml2 bindings. --- .gitignore | 6 + PORTING_PLAN.md | 425 ++++++++++++++++++++++++++++++++++ contrib/python-dmidecode.spec | 5 +- debian/control | 4 +- dmidecode.py | 82 ++++--- examples/dmidump.py | 48 ++-- mock_dmidecodemod.py | 108 +++++++++ src/dmidecodemodule.c | 75 +++--- src/setup_common.py | 6 +- test_elementtree_port.py | 189 +++++++++++++++ test_full_integration.py | 215 +++++++++++++++++ unit-tests/unit | 8 +- 12 files changed, 1080 insertions(+), 91 deletions(-) create mode 100644 PORTING_PLAN.md create mode 100644 mock_dmidecodemod.py create mode 100644 test_elementtree_port.py create mode 100644 test_full_integration.py diff --git a/.gitignore b/.gitignore index 0d20b64..77de6b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ *.pyc +build/ +*.so +*.o +*.a +*.egg-info/ +__pycache__/ diff --git a/PORTING_PLAN.md b/PORTING_PLAN.md new file mode 100644 index 0000000..44e77c1 --- /dev/null +++ b/PORTING_PLAN.md @@ -0,0 +1,425 @@ +# Porting Plan: Migrating from libxml2 to xml.etree.ElementTree + +## Executive Summary + +This document outlines the plan to migrate the python-dmidecode project from using libxml2 via its Python bindings to using xml.etree.ElementTree from the Python standard library. This migration will eliminate the external dependency on libxml2, improving compatibility and simplifying deployment. + +## Implementation Status (Current Working Tree) + +Porting work is in progress on branch `xml-elementtree-port`. + +- Python XML API has been switched to `xml.etree.ElementTree` and now returns wrapper types `dmidecode.XmlNode` / `dmidecode.XmlDoc` (`dmidecode.py`). +- C extension `xmlapi()` no longer returns Python libxml2 objects; it serializes libxml2 `xmlNode` to XML bytes and returns `bytes` to Python (`src/dmidecodemodule.c`). +- Unit tests were updated to validate `dmidecode.XmlNode` / `dmidecode.XmlDoc` instead of `libxml2.xmlNode` / `libxml2.xmlDoc` (`unit-tests/unit`). + +Known gaps/blockers right now: + +- Build still links against `xml2mod` (Python libxml2 bindings helper) via `src/setup_common.py`, which breaks builds on systems without those bindings installed. (Next step: remove this.) +- Python 3 capsule cleanup must free the `options` struct, not the capsule object (fix crash-on-exit / invalid free). +- Example `examples/dmidump.py` still uses libxml2-only APIs (`saveFormatFileEnc()`, `xpathNewContext()`). +- Packaging metadata still depends on Python libxml2 bindings (`debian/control`, `contrib/python-dmidecode.spec`). +- libxml2 C library is still used throughout the C codebase for XML construction (the current work removes Python libxml2 *bindings* usage first). + +## Current State Analysis + +### Current libxml2 Usage + +The project currently uses libxml2 in several key areas: + +1. **Python API** (`dmidecode.py`): + - Imports `xml.etree.ElementTree` (ElementTree) + - Returns wrapper objects (`XmlNode` / `XmlDoc`) around ElementTree objects + - Provides `dmidecodeXML` class with XML query methods that parse XML bytes from the extension + +2. **C Extension** (`src/dmidecodemodule.c`, `src/libxml_wrap.h`): + - Uses libxml2 C API extensively + - Creates XML documents and nodes using libxml2 functions + - `xmlapi()` returns serialized XML (`bytes`) instead of Python libxml2 wrapper objects + +3. **Testing** (`unit-tests/unit`): + - Validates return types as `dmidecode.XmlNode` / `dmidecode.XmlDoc` + +### Key Files Affected + +- `dmidecode.py` - Main Python module +- `unit-tests/unit` - Test suite +- `src/dmidecodemodule.c` - C extension +- `src/libxml_wrap.h` - libxml2 wrapping headers +- `src/setup.py` - Build configuration +- `debian/control`, `contrib/python-dmidecode.spec` - Packaging files + +## SWOT Analysis of Alternative XML Libraries + +### xml.etree.ElementTree (Recommended) + +**Strengths:** +- ✅ Built into Python standard library - no additional dependencies +- ✅ Simple, intuitive API for XML manipulation +- ✅ Good performance for most use cases +- ✅ Lightweight and easy to use +- ✅ Supports XPath-like expressions with `find()` and `findall()` + +**Weaknesses:** +- ❌ No direct equivalent to libxml2's xmlNode/xmlDoc distinction +- ❌ Limited XPath support (only basic subset) +- ❌ Different object model than libxml2 (Element vs Node) + +**Opportunities:** +- 🔄 Can simplify the codebase by removing external dependency +- 🔄 Easier deployment and installation +- 🔄 Better compatibility across different systems + +**Threats:** +- ⚠️ Significant API changes required in both Python and C code +- ⚠️ Potential performance impact for large XML documents +- ⚠️ May need to implement custom wrapping logic + +### xml.dom.minidom (Standard Library Alternative) + +**Strengths:** +- ✅ Built into Python standard library - no additional dependencies +- ✅ DOM-compliant API (closer to libxml2's document/node model) +- ✅ Familiar to developers coming from JavaScript/DOM backgrounds +- ✅ Supports full XML document structure with Document, Node, Element hierarchy +- ✅ Better conceptual match to libxml2's xmlDoc/xmlNode distinction +- ✅ Supports DOM Level 2 Core API + +**Weaknesses:** +- ❌ Slower than ElementTree for most operations (more memory intensive) +- ❌ Verbose API compared to ElementTree +- ❌ No direct equivalent to libxml2's wrapping mechanism +- ❌ Limited XPath support (would need custom implementation) +- ❌ More complex object model with many node types (Element, Text, Comment, etc.) +- ❌ Poor performance with large XML documents due to DOM tree construction + +**Opportunities:** +- 🔄 Closer conceptual match to libxml2's document/node model +- 🔄 Easier migration path for DOM-oriented code +- 🔄 Could provide more familiar API for users expecting DOM-style access +- 🔄 Better support for mixed content and complex XML structures + +**Threats:** +- ⚠️ Performance issues with large DMI data (DMI tables can be substantial) +- ⚠️ Still requires significant code changes from libxml2 +- ⚠️ Less Pythonic API than ElementTree +- ⚠️ Memory consumption could be problematic for embedded systems +- ⚠️ Testing infrastructure would need significant updates + +**Specific Challenges for DMI Data:** +- DMI XML structures tend to be hierarchical but not extremely deep +- Performance impact may be noticeable but potentially acceptable +- Memory usage could be concern for systems with limited resources +- Would need custom wrapper classes to mimic libxml2 API + +### lxml (Third-party Alternative) + +**Strengths:** +- ✅ High performance (written in C) +- ✅ Full XPath 1.0 support +- ✅ Excellent compatibility with ElementTree API +- ✅ Advanced features: XSLT, validation, namespaces +- ✅ Memory efficient +- ✅ Actively maintained +- ✅ Can use both ElementTree and DOM-like interfaces + +**Weaknesses:** +- ❌ External dependency (not in standard library) +- ❌ Larger footprint than standard library options +- ❌ More complex installation (C extensions) +- ❌ Overkill for simple XML needs + +**Why Not Primary Choice:** +While lxml offers excellent performance and features, the standard library options provide better compatibility and simpler deployment for this use case. + +## Comparison: ElementTree vs minidom for DMI Data + +### Decision Factors for python-dmidecode + +| Factor | xml.etree.ElementTree | xml.dom.minidom | Importance | +|--------|----------------------|----------------|------------| +| **Standard Library** | ✅ Yes | ✅ Yes | ⭐⭐⭐⭐⭐ | +| **Performance** | ⚡⚡⚡ (Good) | ⚡ (Poor) | ⭐⭐⭐⭐ | +| **Memory Usage** | 🧠🧠 (Low) | 🧠🧠🧠🧠 (High) | ⭐⭐⭐⭐ | +| **API Simplicity** | ✅✅✅ (Simple) | ❌❌ (Complex) | ⭐⭐⭐ | +| **Conceptual Match** | ❌ (Element-based) | ✅ (Node-based) | ⭐⭐ | +| **XPath Support** | ❌ (Limited) | ❌ (None) | ⭐ | +| **Migration Complexity** | ⚠️⚠️ (Moderate) | ⚠️⚠️⚠️ (High) | ⭐⭐⭐⭐ | +| **Code Maintainability** | ✅✅✅ (High) | ✅✅ (Moderate) | ⭐⭐⭐⭐ | + +### Recommendation Rationale + +**Choose xml.etree.ElementTree because:** + +1. **Performance Matters**: DMI data can be substantial, and ElementTree's better performance is crucial for system tools +2. **Simplicity Wins**: The simpler API will be easier to maintain and extend +3. **Memory Efficiency**: Lower memory usage is important for system-level tools +4. **Modern Python**: ElementTree represents the modern Python approach to XML +5. **Better Trade-off**: While minidom is conceptually closer to libxml2, the performance and simplicity advantages of ElementTree outweigh this benefit + +**When minidom might be considered:** +- If the codebase had extensive DOM manipulation requirements +- If there were many existing users relying on DOM-style API +- If XPath support was critical (though neither standard library option excels here) + +### Performance Considerations for DMI Data + +Typical DMI XML structures: +- **Size**: Usually 10-100KB (can be larger on complex systems) +- **Depth**: 3-6 levels deep +- **Complexity**: Mostly hierarchical with some mixed content +- **Usage Pattern**: Read-heavy, infrequent writes + +ElementTree should handle this workload efficiently, while minidom could show noticeable performance degradation on larger systems. + +## Porting Plan + +### Phase 1: Preparation and Analysis ✅ (Completed) + +1. **Document Current Usage** ✅ + - Identified all libxml2 usage patterns + - Mapped current API surface + - Documented test requirements + +2. **Set Up Development Environment** 🛠️ + - Create a branch for the porting work + - Ensure all tests pass with current libxml2 implementation + - Set up continuous integration for testing + +3. **Create Migration Mapping** 📋 + - Map libxml2 concepts to ElementTree equivalents: + - `libxml2.xmlNode` → `xml.etree.ElementTree.Element` + - `libxml2.xmlDoc` → `xml.etree.ElementTree.ElementTree` (root element) + - libxml2 wrapping functions → Custom Python classes + +### Phase 2: Python API Migration 🐍 + +**Objective**: Update the Python-facing API to use ElementTree + +Status: 🚧 In progress (core API switched; compatibility surface not complete) + +1. **Update dmidecode.py** + ```python + # Replace + import libxml2 + + # With + import xml.etree.ElementTree as ET + + # Create wrapper classes + class XmlNode: + def __init__(self, element): + self.element = element + + class XmlDoc: + def __init__(self, element_tree): + self.element_tree = element_tree + ``` + +2. **Update Result Types** + - Modify `DMIXML_NODE` and `DMIXML_DOC` constants + - Update `SetResultType()` method + - Ensure type checking works with new classes + +3. **Update Query Methods** + - Modify `QuerySection()` and `QueryTypeId()` to return new wrapper objects + - Maintain backward compatibility where possible + +### Phase 3: C Extension Refactoring ⚙️ + +**Objective**: Modify the C extension to work with ElementTree + +Status: 🚧 In progress (XML serialization bridge implemented; build/packaging cleanup pending) + +1. **Replace libxml2 Wrapping** + - Remove `libxml_wrap.h` dependency + - Create new Python object creation functions + - Implement XML serialization/deserialization bridge + +2. **Add XML Serialization Layer** + ```c + // Strategy: Serialize libxml2 XML to string, then parse with ElementTree + char* serialize_libxml2_to_string(xmlNode* node) { + // Implement XML serialization + } + + PyObject* create_elementtree_object(xmlNode* node) { + char* xml_string = serialize_libxml2_to_string(node); + // Call Python ElementTree.parse() on the string + } + ``` + +3. **Update Build System** + - Remove libxml2 dependencies from `setup.py` + - Update build scripts + - Modify packaging metadata + +### Phase 4: Testing Infrastructure Update 🧪 + +**Objective**: Update tests to work with new XML library + +Status: 🚧 In progress (unit test type checks updated; broader behavioral coverage still needed) + +1. **Update unit-tests/unit** + ```python + # Replace + import libxml2 + test(isinstance(output_node, libxml2.xmlNode)) + + # With + from xml.etree.ElementTree import Element + test(isinstance(output_node.element, Element)) + ``` + +2. **Create Test Compatibility Layer** + - Add helper functions to compare XML structures + - Ensure all existing test cases pass + - Update test assertions for new API + +### Phase 5: Gradual Migration Strategy 🎯 + +**Objective**: Implement the migration in manageable steps + +1. **Step 1: Add ElementTree Support Alongside libxml2** + - Create new classes and functions with `_et` suffix + - Allow both APIs to coexist temporarily + - Add feature flag to switch between implementations + +2. **Step 2: Update Documentation** + - Document new API + - Provide migration guide for users + - Update examples to use new API + +3. **Step 3: Deprecate libxml2 API** + - Mark old API as deprecated + - Add warnings for libxml2 usage + - Provide clear migration path + +4. **Step 4: Remove libxml2 Dependency** + - Remove all libxml2-related code + - Update packaging to remove libxml2 dependencies + - Final testing and validation + +### Phase 6: Performance Optimization ⚡ + +**Objective**: Ensure good performance with ElementTree + +1. **Profile XML Processing** + - Identify performance bottlenecks + - Optimize XML serialization/deserialization + +2. **Implement Caching** + - Cache parsed XML structures + - Reduce redundant XML processing + +3. **Memory Management** + - Ensure proper cleanup of XML resources + - Prevent memory leaks in C extension + +### Phase 7: Final Testing and Release 🚀 + +**Objective**: Ensure quality and prepare for release + +1. **Comprehensive Testing** + - Run all unit tests + - Test with various DMI data samples + - Performance benchmarking + +2. **Update Packaging** + - Remove libxml2 from dependencies + - Update setup.py and packaging metadata + - Update distribution packages (RPM, DEB, etc.) + +3. **Release Preparation** + - Update changelog + - Create release notes highlighting the change + - Plan for user communication and support + +## Key Challenges and Solutions + +### Challenge 1: C Extension Compatibility +**Problem**: The C extension currently creates libxml2 objects and wraps them directly. + +**Solution**: +- Add XML serialization layer in C extension +- Convert libxml2 XML to string format +- Parse string with ElementTree in Python layer +- May require temporary dual API support + +### Challenge 2: API Compatibility +**Problem**: Users may have code that expects libxml2.xmlNode/xmlDoc objects. + +**Solution**: +- Provide wrapper classes that mimic libxml2 API +- Offer gradual migration path +- Document breaking changes clearly + +### Challenge 3: Performance Impact +**Problem**: ElementTree may be slower than libxml2 for some operations. + +**Solution**: +- Profile and optimize critical paths +- Consider caching strategies +- Evaluate if performance impact is acceptable + +### Challenge 4: Testing Complexity +**Problem**: Need to ensure all existing functionality works with new XML library. + +**Solution**: +- Maintain comprehensive test suite +- Add XML comparison utilities +- Test with diverse DMI data samples + +## Estimated Timeline + +| Phase | Duration | Status | +|-------|----------|--------| +| Preparation and Analysis | 1-2 days | ✅ Completed | +| Python API Migration | 2-3 days | 🚧 In progress | +| C Extension Refactoring | 3-5 days | 🚧 In progress | +| Testing Updates | 2-3 days | 🚧 In progress | +| Performance Optimization | 2 days | ⏳ Pending | +| Final Testing and Release | 1-2 days | ⏳ Pending | + +**Total Estimate**: 2-3 weeks for complete migration + +## Risk Assessment + +### High Risk Items +- C extension refactoring complexity +- Performance degradation with large DMI datasets +- Breaking changes for existing users + +### Mitigation Strategies +- Implement in phases with backward compatibility +- Performance testing early and often +- Clear communication about breaking changes +- Provide migration guide and tools + +## Migration Checklist + +- [x] Create porting branch +- [~] Update Python API to use ElementTree (core switched; compatibility methods pending) +- [~] Modify C extension for ElementTree compatibility (xmlapi bridge done; build/cleanup pending) +- [~] Update test suite (type checks updated; more assertions pending) +- [ ] Fix build to remove `xml2mod` linking requirement +- [ ] Update examples to work with ElementTree wrappers +- [ ] Drop Python libxml2 binding dependencies from packaging +- [ ] Performance testing and optimization +- [ ] Documentation updates +- [ ] Packaging updates +- [ ] Final integration testing +- [ ] Release preparation + +## Success Criteria + +1. **Functional Equivalence**: All existing functionality works with new XML library +2. **Performance Acceptability**: No significant performance regression +3. **API Compatibility**: Clear migration path for existing users +4. **Test Coverage**: All tests pass with new implementation +5. **Documentation**: Complete and accurate documentation of changes + +## Conclusion + +This porting plan provides a comprehensive approach to migrating from libxml2 to xml.etree.ElementTree. The migration will eliminate the external dependency on libxml2, improving the project's compatibility and maintainability while preserving all existing functionality. + +The recommended approach uses a phased migration strategy to minimize disruption and provide a clear path forward for both developers and users of the python-dmidecode library. diff --git a/contrib/python-dmidecode.spec b/contrib/python-dmidecode.spec index 2c7f942..7e82191 100644 --- a/contrib/python-dmidecode.spec +++ b/contrib/python-dmidecode.spec @@ -10,15 +10,13 @@ Group: System Environment/Libraries URL: http://projects.autonomy.net.au/python-dmidecode/ Source0: http://src.autonomy.net.au/python-dmidecode/%{name}-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root -Requires: libxml2-python -BuildRequires: libxml2-python BuildRequires: libxml2-devel BuildRequires: python-devel %description python-dmidecode is a python extension module that uses the code-base of the 'dmidecode' utility, and presents the data -as python data structures or as XML data using libxml2. +as python data structures or as XML data. %prep %setup -q @@ -121,4 +119,3 @@ rm -rf $RPM_BUILD_ROOT * Sat Mar 7 2009 Clark Williams - 2.10.3-1 - Initial build. - diff --git a/debian/control b/debian/control index 9a0297b..730451f 100644 --- a/debian/control +++ b/debian/control @@ -5,14 +5,14 @@ Priority: optional Homepage: http://projects.autonomy.net.au/python-dmidecode Maintainer: Nima Talebi Build-Depends: debhelper (>> 7), python-support (>= 0.5.3), python, - python-all-dev (>= 2.5.4-2), python-all-dbg, libxml2-dev, python-libxml2 + python-all-dev (>= 2.5.4-2), python-all-dbg, libxml2-dev Standards-Version: 3.8.4 Package: python-dmidecode XB-Python-Version: ${python:Versions} Architecture: any Provides: ${python:Provides} -Depends: ${python:Depends}, ${shlibs:Depends}, ${misc:Depends}, python-libxml2 +Depends: ${python:Depends}, ${shlibs:Depends}, ${misc:Depends} Description: Python extension module for dmidecode DMI (the desktop management interface) provides a standardized description of a computer's hardware, including characteristics such as BIOS serial number diff --git a/dmidecode.py b/dmidecode.py index ac81365..c3d91e0 100644 --- a/dmidecode.py +++ b/dmidecode.py @@ -25,12 +25,40 @@ # are deemed to be part of the source code. # -import libxml2 +import xml.etree.ElementTree as ET from dmidecodemod import * DMIXML_NODE='n' DMIXML_DOC='d' +class XmlNode: + """ + Wrapper class to provide libxml2.xmlNode-like interface using ElementTree.Element + """ + def __init__(self, element): + self.element = element + self._obj = element # Maintain compatibility with libxml2 interface + + def __getattr__(self, name): + """Delegate attribute access to the underlying Element""" + return getattr(self.element, name) + +class XmlDoc: + """ + Wrapper class to provide libxml2.xmlDoc-like interface using ElementTree.ElementTree + """ + def __init__(self, element_tree): + self.element_tree = element_tree + self._obj = element_tree.getroot() # Maintain compatibility + + def getroot(self): + """Get the root element""" + return self.element_tree.getroot() + + def __getattr__(self, name): + """Delegate attribute access to the underlying ElementTree""" + return getattr(self.element_tree, name) + class dmidecodeXML: "Native Python API for retrieving dmidecode information as XML" @@ -40,7 +68,7 @@ def __init__(self): def SetResultType(self, type): """ Sets the result type of queries. The value can be DMIXML_NODE or DMIXML_DOC, - which will return an libxml2::xmlNode or libxml2::xmlDoc object, respectively + which will return an XmlNode or XmlDoc object, respectively """ if type == DMIXML_NODE: @@ -51,39 +79,39 @@ def SetResultType(self, type): raise TypeError("Invalid result type value") return True + def _create_xml_from_string(self, xml_string): + """ + Internal method to create XML objects from string representation + This will be used when the C extension returns XML as strings + """ + try: + element = ET.fromstring(xml_string) + if self.restype == DMIXML_NODE: + return XmlNode(element) + else: # DMIXML_DOC + tree = ET.ElementTree(element) + return XmlDoc(tree) + except ET.ParseError as e: + raise ValueError(f"Failed to parse XML: {e}") from e + def QuerySection(self, sectname): """ Queries the DMI data structure for a given section name. A section can often contain several DMI type elements """ - if self.restype == DMIXML_NODE: - ret = libxml2.xmlNode( _obj = xmlapi(query_type='s', - result_type=self.restype, - section=sectname) ) - elif self.restype == DMIXML_DOC: - ret = libxml2.xmlDoc( _obj = xmlapi(query_type='s', - result_type=self.restype, - section=sectname) ) - else: - raise TypeError("Invalid result type value") - - return ret - + # Get XML data as string from C extension + xml_string = xmlapi('s', self.restype, sectname) + + # Convert to appropriate XML object + return self._create_xml_from_string(xml_string) def QueryTypeId(self, tpid): """ Queries the DMI data structure for a specific DMI type. """ - if self.restype == DMIXML_NODE: - ret = libxml2.xmlNode( _obj = xmlapi(query_type='t', - result_type=self.restype, - typeid=tpid)) - elif self.restype == DMIXML_DOC: - ret = libxml2.xmlDoc( _obj = xmlapi(query_type='t', - result_type=self.restype, - typeid=tpid)) - else: - raise TypeError("Invalid result type value") - - return ret + # Get XML data as string from C extension + xml_string = xmlapi('t', self.restype, tpid) + + # Convert to appropriate XML object + return self._create_xml_from_string(xml_string) diff --git a/examples/dmidump.py b/examples/dmidump.py index 769750d..905e5a4 100755 --- a/examples/dmidump.py +++ b/examples/dmidump.py @@ -145,35 +145,45 @@ def print_warnings(): print() dmixml = dmidecode.dmidecodeXML() -# Fetch all DMI data into a libxml2.xmlDoc object +# Fetch all DMI data into an ElementTree-backed document wrapper print("*** Getting all DMI data into a XML document variable") dmixml.SetResultType(dmidecode.DMIXML_DOC) # Valid values: dmidecode.DMIXML_DOC, dmidecode.DMIXML_NODE xmldoc = dmixml.QuerySection('all') -# Dump the XML to dmidump.xml - formated in UTF-8 decoding +# Dump the XML to dmidump.xml (UTF-8). ElementTree does not pretty-print by default, +# so we indent the tree when available. print("*** Dumping XML document to dmidump.xml") -xmldoc.saveFormatFileEnc('dmidump.xml','UTF-8',1) +try: + import xml.etree.ElementTree as ET + tree = xmldoc.element_tree + if hasattr(ET, 'indent'): + ET.indent(tree, space=' ') + tree.write('dmidump.xml', encoding='utf-8', xml_declaration=True) +except Exception as e: + print("Failed to write dmidump.xml: %s" % e) + +# Do some simple queries on the XML document (ElementTree path syntax) +print("*** Doing some element queries against the XML document") +root = xmldoc.element_tree.getroot() + +keys = ['SystemInfo/Manufacturer', + 'SystemInfo/ProductName', + 'SystemInfo/SerialNumber', + 'SystemInfo/SystemUUID'] -# Do some XPath queries on the XML document -print("*** Doing some XPath queries against the XML document") -dmixp = xmldoc.xpathNewContext() - -# What to look for - XPath expressions -keys = ['/dmidecode/SystemInfo/Manufacturer', - '/dmidecode/SystemInfo/ProductName', - '/dmidecode/SystemInfo/SerialNumber', - '/dmidecode/SystemInfo/SystemUUID'] - -# Extract data and print it for k in keys: - data = dmixp.xpathEval(k) - for d in data: - print("%s: %s" % (k, d.get_content())) + val = root.findtext(k) + print("%s: %s" % (k, val)) -del dmixp del xmldoc # Query for only a particular DMI TypeID - 0x04 - Processor print("*** Quering for Type ID 0x04 - Processor - dumping XML document to stdout") -dmixml.QueryTypeId(0x04).saveFormatFileEnc('-','UTF-8',1) +try: + import xml.etree.ElementTree as ET + x = dmixml.QueryTypeId(0x04) + xml_out = ET.tostring(x.element, encoding='unicode') + print(xml_out) +except Exception as e: + print("Failed to dump XML: %s" % e) print_warnings() diff --git a/mock_dmidecodemod.py b/mock_dmidecodemod.py new file mode 100644 index 0000000..1232e07 --- /dev/null +++ b/mock_dmidecodemod.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +Mock version of dmidecodemod for testing the ElementTree port. +This mock provides the same interface as the C extension but returns test XML data. +""" + +def xmlapi(query_type, result_type, section=None, typeid=None): + """ + Mock xmlapi function that returns test XML data as strings. + This simulates what the modified C extension will return. + """ + + # Test XML data for different query types + test_data = { + 'bios': """ + + + Test BIOS Vendor + 1.2.3 + 01/01/2023 + + """, + + 'system': """ + + + Test Manufacturer + Test Product + 1.0 + TEST123456 + + """, + + 'baseboard': """ + + + Test Board Manufacturer + Test Board + 1.0 + BOARD123 + + """, + + '0': """ + + + Test BIOS Vendor + 1.2.3 + 01/01/2023 + + """, + + '1': """ + + + Test Manufacturer + Test Product + 1.0 + TEST123456 + + """, + + '2': """ + + + Test Board Manufacturer + Test Board + 1.0 + BOARD123 + + """, + } + + # Determine what data to return based on query type + if query_type == 's': # Section query + if section in test_data: + return test_data[section] + else: + # Return a generic response for unknown sections + return f""" + + <{section}_information> + Test Manufacturer + + """ + + elif query_type == 't': # Type ID query + typeid_str = str(typeid) + if typeid_str in test_data: + return test_data[typeid_str] + else: + # Return a generic response for unknown types + return f""" + + + Test Manufacturer + + """ + + else: + return """ + + Unknown query type + """ + +# Mock other functions that might be imported +version = "3.12.3 (ElementTree Port)" +dmi = "Test DMI String" \ No newline at end of file diff --git a/src/dmidecodemodule.c b/src/dmidecodemodule.c index efa730b..18e4184 100644 --- a/src/dmidecodemodule.c +++ b/src/dmidecodemodule.c @@ -42,7 +42,6 @@ #include #include -#include "libxml_wrap.h" #include "dmidecodemodule.h" #include "dmixml.h" @@ -479,7 +478,7 @@ xmlNode *__dmidecode_xml_getsection(options *opt, const char *section) { if(opt->type == -1) { char *err = log_retrieve(opt->logdata, LOG_ERR); log_clear_partial(opt->logdata, LOG_ERR, 0); - _pyReturnError(PyExc_RuntimeError, "Invalid type id '%s' -- %s", typeid, err); + _pyReturnError(PyExc_RuntimeError, __FILE__, __LINE__, "Invalid type id '%s' -- %s", typeid, err); free(err); return NULL; } @@ -690,18 +689,18 @@ static PyObject *dmidecode_get_type(PyObject * self, PyObject * args) return pydata; } -static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args, PyObject *keywds) +static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args) { - static char *keywordlist[] = {"query_type", "result_type", "section", "typeid", NULL}; PyObject *pydata = NULL; - xmlDoc *dmixml_doc = NULL; + xmlDoc *temp_doc = NULL; xmlNode *dmixml_n = NULL; + xmlChar *xml_buffer = NULL; char *sect_query = NULL, *qtype = NULL, *rtype = NULL; int type_query = -1; + int buffer_size = 0; - // Parse the keywords - we only support keywords, as this is an internal API - if( !PyArg_ParseTupleAndKeywords(args, keywds, "ss|si", keywordlist, - &qtype, &rtype, §_query, &type_query) ) { + // Parse arguments - we use a simpler interface for compatibility + if( !PyArg_ParseTuple(args, "ss|si", &qtype, &rtype, §_query, &type_query) ) { return NULL; } @@ -735,27 +734,35 @@ static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args, PyObject *keyw return NULL; } - // Check for sensible return type and wrap the correct type into a Python Object - switch( *rtype ) { - case 'n': - pydata = libxml_xmlNodePtrWrap((xmlNode *) dmixml_n); - break; - - case 'd': - dmixml_doc = xmlNewDoc((xmlChar *) "1.0"); - if( dmixml_doc == NULL ) { - PyReturnError(PyExc_MemoryError, "Could not create new XML document"); - } - xmlDocSetRootElement(dmixml_doc, dmixml_n); - pydata = libxml_xmlDocPtrWrap((xmlDoc *) dmixml_doc); - break; - - default: - PyReturnError(PyExc_TypeError, "Internal error - invalid result type '%c'", *rtype); + // Convert the XML node to a string representation + // Use the variables declared at function scope + + // Create a temporary document to hold our node + temp_doc = xmlNewDoc((xmlChar *) "1.0"); + if( temp_doc == NULL ) { + PyReturnError(PyExc_MemoryError, "Could not create temporary XML document"); + } + + // Set the node as root element + xmlDocSetRootElement(temp_doc, dmixml_n); + + // Serialize the document to a string buffer + xmlDocDumpMemory(temp_doc, &xml_buffer, &buffer_size); + + // Free the temporary document (this doesn't free the original node) + xmlFreeDoc(temp_doc); + + // Create Python string from the XML buffer + pydata = PyBytes_FromStringAndSize((const char *)xml_buffer, buffer_size); + + // Free the XML buffer + xmlFree(xml_buffer); + + if( pydata == NULL ) { + PyReturnError(PyExc_MemoryError, "Could not create Python string from XML"); } - // Return XML data - Py_INCREF(pydata); + // Return XML data as string return pydata; } @@ -913,7 +920,7 @@ static PyMethodDef DMIDataMethods[] = { {(char *)"pythonmap", dmidecode_set_pythonxmlmap, METH_O, (char *) "Use another python dict map definition. The default file is " PYTHON_XML_MAP}, - {(char *)"xmlapi", dmidecode_xmlapi, METH_VARARGS | METH_KEYWORDS, + {(char *)"xmlapi", dmidecode_xmlapi, METH_VARARGS, (char *) "Internal API for retrieving data as raw XML data"}, @@ -926,12 +933,16 @@ static PyMethodDef DMIDataMethods[] = { {NULL, NULL, 0, NULL} }; -void destruct_options(void *ptr) +void destruct_options(PyObject *ptr) { + void *actual_ptr = ptr; #ifdef IS_PY3K - ptr = PyCapsule_GetPointer(ptr, NULL); + actual_ptr = PyCapsule_GetPointer(ptr, NULL); #endif - options *opt = (options *) ptr; + if( actual_ptr == NULL ) { + return; + } + options *opt = (options *) actual_ptr; if( opt->mappingxml != NULL ) { xmlFreeDoc(opt->mappingxml); @@ -965,7 +976,7 @@ void destruct_options(void *ptr) log_close(opt->logdata); } - free(ptr); + free(actual_ptr); } #ifdef IS_PY3K diff --git a/src/setup_common.py b/src/setup_common.py index aec1f9b..94e2b6b 100644 --- a/src/setup_common.py +++ b/src/setup_common.py @@ -68,8 +68,9 @@ def libxml2_lib(libdir, libs): elif l.find('-l') == 0: libs.append(l.replace("-l", "", 1)) - # this library is not reported and we need it anyway - libs.append('xml2mod') + # Historically we linked against the Python libxml2 bindings helper library + # (xml2mod) to expose libxml2 objects to Python. The ElementTree port no + # longer relies on those bindings, so do not force-link against xml2mod. @@ -100,4 +101,3 @@ def get_macros(): if sys.byteorder == 'big': macros.append(("ALIGNMENT_WORKAROUND", None)) return macros - diff --git a/test_elementtree_port.py b/test_elementtree_port.py new file mode 100644 index 0000000..0aaa713 --- /dev/null +++ b/test_elementtree_port.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +Test script to verify the ElementTree port works correctly. +This tests the Python API changes without requiring the C extension to be rebuilt. +""" + +import sys +import os + +# Add the current directory to Python path so we can import our modified dmidecode +sys.path.insert(0, '.') + +# Mock the dmidecodemod module before importing dmidecode +import mock_dmidecodemod +sys.modules['dmidecodemod'] = mock_dmidecodemod + +def test_imports(): + """Test that we can import the module and it uses ElementTree""" + try: + import xml.etree.ElementTree as ET + print("✅ Successfully imported xml.etree.ElementTree") + + # Try to import our modified dmidecode + import dmidecode + print("✅ Successfully imported dmidecode module") + + # Check that it's using our new classes + assert hasattr(dmidecode, 'XmlNode'), "XmlNode class not found" + assert hasattr(dmidecode, 'XmlDoc'), "XmlDoc class not found" + print("✅ Found XmlNode and XmlDoc classes") + + return True + + except ImportError as e: + print(f"❌ Import failed: {e}") + return False + except AssertionError as e: + print(f"❌ Assertion failed: {e}") + return False + +def test_wrapper_classes(): + """Test that our wrapper classes work correctly""" + try: + import dmidecode + import xml.etree.ElementTree as ET + + # Create a test XML element + test_xml = """ + + + Test Manufacturer + Test Product + + """ + + # Parse with ElementTree + root = ET.fromstring(test_xml) + + # Test XmlNode wrapper + xml_node = dmidecode.XmlNode(root) + assert hasattr(xml_node, 'element'), "XmlNode missing element attribute" + assert hasattr(xml_node, '_obj'), "XmlNode missing _obj attribute" + print("✅ XmlNode wrapper class works correctly") + + # Test XmlDoc wrapper + tree = ET.ElementTree(root) + xml_doc = dmidecode.XmlDoc(tree) + assert hasattr(xml_doc, 'element_tree'), "XmlDoc missing element_tree attribute" + assert hasattr(xml_doc, '_obj'), "XmlDoc missing _obj attribute" + assert hasattr(xml_doc, 'getroot'), "XmlDoc missing getroot method" + print("✅ XmlDoc wrapper class works correctly") + + return True + + except Exception as e: + print(f"❌ Wrapper class test failed: {e}") + return False + +def test_dmidecode_xml_class(): + """Test that the dmidecodeXML class works with our changes""" + try: + import dmidecode + + # Create instance + dmi_xml = dmidecode.dmidecodeXML() + assert dmi_xml.restype == dmidecode.DMIXML_NODE, "Default restype should be DMIXML_NODE" + print("✅ dmidecodeXML instance created successfully") + + # Test SetResultType + result = dmi_xml.SetResultType(dmidecode.DMIXML_NODE) + assert result is True, "SetResultType should return True" + assert dmi_xml.restype == dmidecode.DMIXML_NODE + + result = dmi_xml.SetResultType(dmidecode.DMIXML_DOC) + assert result is True, "SetResultType should return True" + assert dmi_xml.restype == dmidecode.DMIXML_DOC + print("✅ SetResultType method works correctly") + + # Test invalid type + try: + dmi_xml.SetResultType('invalid') + print("❌ SetResultType should have raised TypeError for invalid type") + return False + except TypeError: + print("✅ SetResultType correctly rejects invalid types") + + return True + + except Exception as e: + print(f"❌ dmidecodeXML class test failed: {e}") + return False + +def test_xml_parsing(): + """Test that our XML parsing method works""" + try: + import dmidecode + + # Create instance + dmi_xml = dmidecode.dmidecodeXML() + + # Test XML string parsing + test_xml = """ + + Test Manufacturer + Test Product + """ + + # Test with DMIXML_NODE + dmi_xml.SetResultType(dmidecode.DMIXML_NODE) + result = dmi_xml._create_xml_from_string(test_xml) + assert isinstance(result, dmidecode.XmlNode), f"Expected XmlNode, got {type(result)}" + assert hasattr(result, 'element'), "Result should have element attribute" + print("✅ XML parsing for DMIXML_NODE works correctly") + + # Test with DMIXML_DOC + dmi_xml.SetResultType(dmidecode.DMIXML_DOC) + result = dmi_xml._create_xml_from_string(test_xml) + assert isinstance(result, dmidecode.XmlDoc), f"Expected XmlDoc, got {type(result)}" + assert hasattr(result, 'element_tree'), "Result should have element_tree attribute" + print("✅ XML parsing for DMIXML_DOC works correctly") + + # Test invalid XML + try: + dmi_xml._create_xml_from_string(" ", 1) failed() @@ -330,7 +330,7 @@ try: vwrite(" * XML: Testing dmidecodeXML::QueryTypeId(%s)..." % red(i), 1) try: output_node = dmixml.QueryTypeId(i) - test(isinstance(output_node, libxml2.xmlNode)) + test(isinstance(output_node, dmidecode.XmlNode)) except Exception as e: failed(e, 1) except: @@ -347,7 +347,7 @@ try: ), 1) try: output_doc = dmixml.QuerySection(section) - test(isinstance(output_doc, libxml2.xmlDoc)) + test(isinstance(output_doc, dmidecode.XmlDoc)) except Exception as e: failed(e, 1) except: From 29d0081d6cf5ec9f262ae984cd3b884ce128f85b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Mon, 2 Mar 2026 22:59:03 +0100 Subject: [PATCH 2/7] Fix mock xmlapi typeid argument handling Update the test mock to accept the simplified positional xmlapi('t', rtype, typeid) call shape used by the port so the integration test reflects the real extension behavior. --- mock_dmidecodemod.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mock_dmidecodemod.py b/mock_dmidecodemod.py index 1232e07..6a7d292 100644 --- a/mock_dmidecodemod.py +++ b/mock_dmidecodemod.py @@ -85,6 +85,10 @@ def xmlapi(query_type, result_type, section=None, typeid=None): """ elif query_type == 't': # Type ID query + # The real extension passes typeid as a positional argument in the + # third slot when using the simplified xmlapi('t', rtype, tpid) API. + if typeid is None and section is not None: + typeid = section typeid_str = str(typeid) if typeid_str in test_data: return test_data[typeid_str] @@ -105,4 +109,4 @@ def xmlapi(query_type, result_type, section=None, typeid=None): # Mock other functions that might be imported version = "3.12.3 (ElementTree Port)" -dmi = "Test DMI String" \ No newline at end of file +dmi = "Test DMI String" From 53f30e5bca504d2e3a13d8eb8c49256b4cba4156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Mon, 2 Mar 2026 23:12:18 +0100 Subject: [PATCH 3/7] Fix xmlapi arg parsing and build/test paths Accept legacy xmlapi() call shapes and make Makefile/unit tests resilient to modern Python build directories. --- Makefile | 30 ++++++++++++++------------- PORTING_PLAN.md | 10 ++++----- src/dmidecodemodule.c | 47 +++++++++++++++++++++++++++++++++++++++---- unit-tests/unit | 36 +++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 2f47a75..92aff6a 100644 --- a/Makefile +++ b/Makefile @@ -38,18 +38,21 @@ #. $AutoHeaderSerial::20100225 $ #. ******* AUTOHEADER END v1.2 ******* -PY_BIN := python3 +PY_BIN ?= python3 VERSION := $(shell cd src;$(PY_BIN) -c "from setup_common import *; print(get_version());") PACKAGE := python-dmidecode PY_VER := $(shell $(PY_BIN) -c 'import sys; print("%d.%d"%sys.version_info[0:2])') -PY_MV := $(shell echo $(PY_VER) | cut -b 1) -PY := python$(PY_VER) -SO_PATH := build/lib.linux-$(shell uname -m)-$(PY_VER) +PY_MV := $(shell $(PY_BIN) -c 'import sys; print(sys.version_info[0])') +PY_TAG := $(shell $(PY_BIN) -c 'import sys; print("python%d.%d"%sys.version_info[0:2])') + ifeq ($(PY_MV),2) - SO := $(SO_PATH)/dmidecodemod.so + PLATFORM := $(shell $(PY_BIN) -c 'from distutils.util import get_platform; print(get_platform())') + BUILD_LIB := build/lib.$(PLATFORM)-$(PY_VER) + SO := $(BUILD_LIB)/dmidecodemod.so else - SOABI := $(shell $(PY_BIN) -c 'import sysconfig; print(sysconfig.get_config_var("SOABI"))') - SO := $(SO_PATH)/dmidecodemod.$(SOABI).so + SOABI := $(shell $(PY_BIN) -c 'import sysconfig; print(sysconfig.get_config_var("SOABI") or "")') + BUILD_LIB := $(shell $(PY_BIN) -c 'import sys, sysconfig; print("build/lib.%s-%s" % (sysconfig.get_platform(), sys.implementation.cache_tag))') + SO := $(BUILD_LIB)/dmidecodemod.$(SOABI).so endif SHELL := /bin/bash @@ -58,23 +61,23 @@ SHELL := /bin/bash all : build dmidump -build: $(PY)-dmidecodemod.so -$(PY)-dmidecodemod.so: $(SO) +build: $(PY_TAG)-dmidecodemod.so +$(PY_TAG)-dmidecodemod.so: $(SO) cp $< $@ $(SO): - $(PY) src/setup.py build + $(PY_BIN) src/setup.py build dmidump : src/util.o src/efi.o src/dmilog.o $(CC) -o $@ src/dmidump.c $^ -g -Wall -D_DMIDUMP_MAIN_ install: - $(PY) src/setup.py install + $(PY_BIN) src/setup.py install uninstall: - $(PY) src/setup.py uninstall + $(PY_BIN) src/setup.py uninstall clean: - -$(PY) src/setup.py clean --all + -$(PY_BIN) src/setup.py clean --all -rm -f *.so lib/*.o core dmidump src/*.o -rm -rf build -rm -rf rpm @@ -111,4 +114,3 @@ conflicts: @comm -12 \ <(dpkg-deb -c ../../DPKGS/python-dmidecode_$(VERSION)-1_amd64.deb | awk '$$NF!~/\/$$/{print$$NF}'|sort) \ <(dpkg-deb -c ../../DPKGS/python-dmidecode-dbg_$(VERSION)-1_amd64.deb | awk '$$NF!~/\/$$/{print$$NF}'|sort) - diff --git a/PORTING_PLAN.md b/PORTING_PLAN.md index 44e77c1..7fac2ec 100644 --- a/PORTING_PLAN.md +++ b/PORTING_PLAN.md @@ -201,15 +201,15 @@ Status: 🚧 In progress (core API switched; compatibility surface not complete) ```python # Replace import libxml2 - + # With import xml.etree.ElementTree as ET - + # Create wrapper classes class XmlNode: def __init__(self, element): self.element = element - + class XmlDoc: def __init__(self, element_tree): self.element_tree = element_tree @@ -241,7 +241,7 @@ Status: 🚧 In progress (XML serialization bridge implemented; build/packaging char* serialize_libxml2_to_string(xmlNode* node) { // Implement XML serialization } - + PyObject* create_elementtree_object(xmlNode* node) { char* xml_string = serialize_libxml2_to_string(node); // Call Python ElementTree.parse() on the string @@ -264,7 +264,7 @@ Status: 🚧 In progress (unit test type checks updated; broader behavioral cove # Replace import libxml2 test(isinstance(output_node, libxml2.xmlNode)) - + # With from xml.etree.ElementTree import Element test(isinstance(output_node.element, Element)) diff --git a/src/dmidecodemodule.c b/src/dmidecodemodule.c index 18e4184..e3cf5cb 100644 --- a/src/dmidecodemodule.c +++ b/src/dmidecodemodule.c @@ -695,25 +695,64 @@ static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args) xmlDoc *temp_doc = NULL; xmlNode *dmixml_n = NULL; xmlChar *xml_buffer = NULL; - char *sect_query = NULL, *qtype = NULL, *rtype = NULL; + const char *sect_query = NULL, *qtype = NULL, *rtype = NULL; + PyObject *third_arg = NULL; int type_query = -1; int buffer_size = 0; - // Parse arguments - we use a simpler interface for compatibility - if( !PyArg_ParseTuple(args, "ss|si", &qtype, &rtype, §_query, &type_query) ) { + // Parse arguments. + // We support both of these call shapes: + // xmlapi('s', rtype, section) + // xmlapi('t', rtype, typeid) + // And the legacy 4-arg variant: + // xmlapi('t', rtype, section_placeholder, typeid) + if( !PyArg_ParseTuple(args, "ss|Oi", &qtype, &rtype, &third_arg, &type_query) ) { return NULL; } + if( third_arg == Py_None ) { + third_arg = NULL; + } + // Check for sensible arguments and retrieve the xmlNode with DMI data switch( *qtype ) { case 's': // Section / GroupName + if( third_arg == NULL ) { + PyReturnError(PyExc_TypeError, "section argument cannot be NULL") + } + if( PyUnicode_Check(third_arg) ) { + sect_query = PyUnicode_AsUTF8(third_arg); + } else if( PyBytes_Check(third_arg) ) { + sect_query = PyBytes_AsString(third_arg); + } else { + PyReturnError(PyExc_TypeError, "section argument must be str or bytes") + } if( sect_query == NULL ) { - PyReturnError(PyExc_TypeError, "section keyword cannot be NULL") + // Exception already set by PyUnicode_AsUTF8() or PyBytes_AsString() + return NULL; } dmixml_n = __dmidecode_xml_getsection(global_options, sect_query); break; case 't': // TypeID / direct TypeMap + // Prefer a positional typeid in the third slot. + if( third_arg != NULL ) { + if( PyLong_Check(third_arg) ) { + long v = PyLong_AsLong(third_arg); + if( PyErr_Occurred() ) { + return NULL; + } + type_query = (int) v; + } else if( type_query < 0 && (PyUnicode_Check(third_arg) || PyBytes_Check(third_arg)) ) { + // Backwards compatibility: allow typeid passed as string. + const char *s = PyUnicode_Check(third_arg) ? PyUnicode_AsUTF8(third_arg) + : PyBytes_AsString(third_arg); + if( s == NULL ) { + return NULL; + } + type_query = atoi(s); + } + } if( type_query < 0 ) { PyReturnError(PyExc_TypeError, "typeid keyword must be set and must be a positive integer"); diff --git a/unit-tests/unit b/unit-tests/unit index fded561..576ad66 100755 --- a/unit-tests/unit +++ b/unit-tests/unit @@ -2,6 +2,7 @@ #.awk '$0 ~ /case [0-9]+: .. 3/ { sys.stdout.write($2 }' src/dmidecode.c|tr ':\n' ', ' from pprint import pprint +import glob import os, sys, subprocess, random, tempfile, time if sys.version_info[0] < 3: import commands as subprocess @@ -138,6 +139,41 @@ def vwrite(msg, vLevel=0): ################################################################################ +# Setup temporary sys.path() with our build dir + +def _add_build_paths(): + """Make unit tests resilient across Python versions/build layouts. + + Historically this test suite assumed a distutils build directory like: + ../build/lib.-- + + On newer Pythons the build directory often looks like: + ../build/lib.- + (e.g. lib.linux-x86_64-cpython-313) + """ + + here = os.path.dirname(os.path.abspath(__file__)) + repo_root = os.path.abspath(os.path.join(here, '..')) + build_root = os.path.join(repo_root, 'build') + + # Always allow importing from repo root (useful for in-place builds) + if repo_root not in sys.path: + sys.path.insert(0, repo_root) + + # Prefer any build/lib.* directory that contains dmidecode.py + candidates = sorted(glob.glob(os.path.join(build_root, 'lib.*'))) + for p in candidates: + if os.path.exists(os.path.join(p, 'dmidecode.py')) and p not in sys.path: + sys.path.insert(0, p) + + # Fallback: add all build/lib.* candidates (best-effort) + for p in candidates: + if p not in sys.path: + sys.path.insert(0, p) + + +_add_build_paths() + #. Let's ignore warnings from the module for the test units... err = open('/dev/null', 'a+', 1) os.dup2(err.fileno(), sys.stderr.fileno()) From 8e3c47a0ba17b0f03d15a781e910cb869d2f605d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Mon, 2 Mar 2026 23:33:27 +0100 Subject: [PATCH 4/7] Fix XML output for ElementTree parsing --- src/dmidecode.c | 7 +++++-- src/dmixml.c | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/dmidecode.c b/src/dmidecode.c index d40f0ee..714672d 100644 --- a/src/dmidecode.c +++ b/src/dmidecode.c @@ -1790,7 +1790,11 @@ void dmi_memory_module_error(xmlNode *node, u8 code) dmixml_AddAttribute(data_n, "flags", "0x%04x", code); if( !(code & (1 << 2)) ) { - dmixml_AddAttribute(data_n, "Error Status", "%s", status[code & 0x03]); + dmixml_AddAttribute(data_n, "Error", "%i", ((code & 0x03) == 0 ? 0 : 1)); + dmixml_AddAttribute(data_n, "Status", "%s", status[code & 0x03]); + } else { + dmixml_AddAttribute(data_n, "unknown", "1"); + dmixml_AddAttribute(data_n, "Error", "0"); } } @@ -6423,4 +6427,3 @@ int legacy_decode(Log_t *logp, int type, u8 *buf, const char *devmem, u32 flags, ((buf[0x0E] & 0xF0) << 4) + (buf[0x0E] & 0x0F), devmem, flags, xmlnode); return check; } - diff --git a/src/dmixml.c b/src/dmixml.c index 682acc7..23d495b 100644 --- a/src/dmixml.c +++ b/src/dmixml.c @@ -45,6 +45,28 @@ #include "dmilog.h" #include "dmixml.h" +static void dmixml_sanitize_xml_string(xmlChar *s) +{ + /* + * libxml2 expects UTF-8 strings. SMBIOS strings are often plain bytes + * with vendor-specific encodings; make output safe for XML parsers by + * restricting to XML 1.0-safe ASCII. + */ + if (s == NULL) { + return; + } + + for (; *s; s++) { + unsigned char c = (unsigned char)*s; + if (c == '\t' || c == '\n' || c == '\r') { + continue; + } + if (c < 0x20 || c == 0x7F || c >= 0x80) { + *s = (xmlChar)'.'; + } + } +} + /** * Internal function for dmixml_* functions. The function will allocate a buffer and populate it * according to the format string @@ -68,6 +90,8 @@ xmlChar *dmixml_buildstr(size_t len, const char *fmt, va_list ap) { xmlStrVPrintf(ret, len, xmlfmt, ap); free(xmlfmt); + dmixml_sanitize_xml_string(ret); + // Right trim the string ptr = ret + xmlStrlen(ret)-1; while( (ptr >= ret) && (*ptr == ' ') ) { @@ -201,6 +225,7 @@ xmlNode *dmixml_AddDMIstring(xmlNode *node, const char *tagname, const struct dm xmlChar *ret = NULL; xmlChar *ptr = NULL; xmlChar *val_s = xmlCharStrdup(dmistr); + dmixml_sanitize_xml_string(val_s); // Right trim the string ret = val_s; ptr = ret + xmlStrlen(ret) - 1; From 81dd65744e11924423b403579c055d72c488284b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Mon, 2 Mar 2026 23:33:36 +0100 Subject: [PATCH 5/7] Remove Travis config and update ElementTree port notes --- .travis.yml | 25 ------------------------- Makefile | 6 ++---- PORTING_PLAN.md | 9 +++++---- contrib/python-dmidecode.spec | 2 +- src/dmidecodemodule.c | 1 - 5 files changed, 8 insertions(+), 35 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cb46c7e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: c -sudo: true - -env: - global: - - CODECOV_TOKEN=4792f32d-1685-4e2d-8cc4-b82e9578a605 - -before_install: - - sudo apt-get update -qq - - sudo apt-get install python-libxml2 libxml2-dev python-dev - -script: - - sed 's/$(CC)/$(CC) -coverage/g' Makefile > t_makefile - - cp t_makefile Makefile - - rm -f t_makefile - - make GCOV=1 build - - make GCOV=1 unit - - find build/ -name '*.gcno' -exec mv {} ./ \; - - find build/ -name '*.gcda' -exec mv {} ./ \; - - make GCOV=1 dmidump - - sudo ./dmidump /dev/mem /dev/null - - make GCOV=1 version - -after_success: - - bash <(curl -s https://codecov.io/bash) -F unittest diff --git a/Makefile b/Makefile index 92aff6a..7bd4060 100644 --- a/Makefile +++ b/Makefile @@ -61,11 +61,9 @@ SHELL := /bin/bash all : build dmidump -build: $(PY_TAG)-dmidecodemod.so -$(PY_TAG)-dmidecodemod.so: $(SO) - cp $< $@ -$(SO): +build: $(PY_BIN) src/setup.py build + cp "$(SO)" "$(PY_TAG)-dmidecodemod.so" dmidump : src/util.o src/efi.o src/dmilog.o $(CC) -o $@ src/dmidump.c $^ -g -Wall -D_DMIDUMP_MAIN_ diff --git a/PORTING_PLAN.md b/PORTING_PLAN.md index 7fac2ec..87c614b 100644 --- a/PORTING_PLAN.md +++ b/PORTING_PLAN.md @@ -14,10 +14,11 @@ Porting work is in progress on branch `xml-elementtree-port`. Known gaps/blockers right now: -- Build still links against `xml2mod` (Python libxml2 bindings helper) via `src/setup_common.py`, which breaks builds on systems without those bindings installed. (Next step: remove this.) +- Build no longer force-links `xml2mod` via `src/setup_common.py` (no Python libxml2 bindings required). - Python 3 capsule cleanup must free the `options` struct, not the capsule object (fix crash-on-exit / invalid free). -- Example `examples/dmidump.py` still uses libxml2-only APIs (`saveFormatFileEnc()`, `xpathNewContext()`). -- Packaging metadata still depends on Python libxml2 bindings (`debian/control`, `contrib/python-dmidecode.spec`). +- Example `examples/dmidump.py` updated to use ElementTree wrappers. +- CI/packaging metadata must not require Python libxml2 bindings. +- CI configuration still installs Python libxml2 bindings (`.travis.yml`). - libxml2 C library is still used throughout the C codebase for XML construction (the current work removes Python libxml2 *bindings* usage first). ## Current State Analysis @@ -401,7 +402,7 @@ Status: 🚧 In progress (unit test type checks updated; broader behavioral cove - [~] Update Python API to use ElementTree (core switched; compatibility methods pending) - [~] Modify C extension for ElementTree compatibility (xmlapi bridge done; build/cleanup pending) - [~] Update test suite (type checks updated; more assertions pending) -- [ ] Fix build to remove `xml2mod` linking requirement +- [x] Fix build to remove `xml2mod` linking requirement - [ ] Update examples to work with ElementTree wrappers - [ ] Drop Python libxml2 binding dependencies from packaging - [ ] Performance testing and optimization diff --git a/contrib/python-dmidecode.spec b/contrib/python-dmidecode.spec index 7e82191..f0dd707 100644 --- a/contrib/python-dmidecode.spec +++ b/contrib/python-dmidecode.spec @@ -103,7 +103,7 @@ rm -rf $RPM_BUILD_ROOT - Only build the python-dmidecode module, not everything * Wed Jul 13 2009 David Sommerseth - 3.10.6-5 -- Added missing BuildRequres for libxml2-python +- Added missing BuildRequres for libxml2-python (historical; no longer required for ElementTree port) * Wed Jul 13 2009 David Sommerseth - 3.10.6-4 - Added missing BuildRequres for python-devel diff --git a/src/dmidecodemodule.c b/src/dmidecodemodule.c index e3cf5cb..d594ead 100644 --- a/src/dmidecodemodule.c +++ b/src/dmidecodemodule.c @@ -1073,7 +1073,6 @@ initdmidecodemod(void) // Assign this options struct to the module as well with a destructor, that way it will // clean up the memory for us. - // TODO: destructor has wrong type under py3? PyModule_AddObject(module, "options", PyCapsule_New(opt, NULL, destruct_options)); global_options = opt; #ifdef IS_PY3K From d80cb15caa90a147dc48b80df0470c80eb650b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Tue, 3 Mar 2026 01:13:09 +0100 Subject: [PATCH 6/7] fix: make `make build` work --- src/dmidecodemodule.c | 8 ++++---- src/dmidump.c | 3 ++- src/dmixml.c | 9 +++------ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/dmidecodemodule.c b/src/dmidecodemodule.c index d594ead..78a4ef7 100644 --- a/src/dmidecodemodule.c +++ b/src/dmidecodemodule.c @@ -656,7 +656,7 @@ static PyObject *dmidecode_get_slot(PyObject * self, PyObject * args) static PyObject *dmidecode_get_section(PyObject *self, PyObject *args) { - char *section = NULL; + const char *section = NULL; if (PyUnicode_Check(args)) { section = PyUnicode_AsUTF8(args); } else if (PyBytes_Check(args)) { @@ -834,7 +834,7 @@ static PyObject *dmidecode_get_dev(PyObject * self, PyObject * null) static PyObject *dmidecode_set_dev(PyObject * self, PyObject * arg) { - char *f = NULL; + const char *f = NULL; if(PyUnicode_Check(arg)) { f = PyUnicode_AsUTF8(arg); } else if(PyBytes_Check(arg)) { @@ -881,7 +881,7 @@ static PyObject *dmidecode_set_dev(PyObject * self, PyObject * arg) static PyObject *dmidecode_set_pythonxmlmap(PyObject * self, PyObject * arg) { - char *fname = NULL; + const char *fname = NULL; if (PyUnicode_Check(arg)) { fname = PyUnicode_AsUTF8(arg); @@ -1044,7 +1044,7 @@ initdmidecodemod(void) options *opt; xmlInitParser(); - xmlXPathInit(); + /* xmlXPathInit() is deprecated (no longer needed with libxml2 init) */ opt = (options *) malloc(sizeof(options)+2); if (opt == NULL) diff --git a/src/dmidump.c b/src/dmidump.c index 07f24e9..2f971c9 100644 --- a/src/dmidump.c +++ b/src/dmidump.c @@ -152,7 +152,8 @@ static int legacy_decode(u8 *buf, const char *devmem, u32 flags, const char *du devmem, flags, dumpfile); memcpy(crafted, buf, 16); - overwrite_smbios3_address(crafted); + /* Legacy entry point is a DMI entry point, not SMBIOS3 */ + overwrite_dmi_address(crafted); write_dump(0, 0x0F, crafted, dumpfile, 1); return 1; diff --git a/src/dmixml.c b/src/dmixml.c index 23d495b..9fa4008 100644 --- a/src/dmixml.c +++ b/src/dmixml.c @@ -77,18 +77,15 @@ static void dmixml_sanitize_xml_string(xmlChar *s) * @return xmlChar* Pointer to the buffer of the string */ xmlChar *dmixml_buildstr(size_t len, const char *fmt, va_list ap) { - xmlChar *ret = NULL, *xmlfmt = NULL; + xmlChar *ret = NULL; xmlChar *ptr = NULL; ret = (xmlChar *) malloc(len+2); assert( ret != NULL ); memset(ret, 0, len+2); - xmlfmt = xmlCharStrdup(fmt); - assert( xmlfmt != NULL ); - - xmlStrVPrintf(ret, len, xmlfmt, ap); - free(xmlfmt); + /* xmlStrVPrintf expects a const char * format string */ + xmlStrVPrintf(ret, len, (const char *)fmt, ap); dmixml_sanitize_xml_string(ret); From 0c6c9a27598eb1607f99aebaae4f6427f9ba3b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Mon, 9 Mar 2026 17:08:59 +0100 Subject: [PATCH 7/7] fix: restore xmlapi kwarg compatibility Accept xmlapi() keyword arguments again (while keeping positional call forms) and avoid __getattr__ recursion in ElementTree wrapper classes. Update docs/tests and silence cast-function-type warnings for the METH_KEYWORDS entry. --- PORTING_PLAN.md | 4 +- dmidecode.py | 9 +-- src/dmidecodemodule.c | 127 ++++++++++++++++++++++++++++++++---------- unit-tests/unit | 5 -- 4 files changed, 105 insertions(+), 40 deletions(-) diff --git a/PORTING_PLAN.md b/PORTING_PLAN.md index 87c614b..5ad1244 100644 --- a/PORTING_PLAN.md +++ b/PORTING_PLAN.md @@ -9,7 +9,7 @@ This document outlines the plan to migrate the python-dmidecode project from usi Porting work is in progress on branch `xml-elementtree-port`. - Python XML API has been switched to `xml.etree.ElementTree` and now returns wrapper types `dmidecode.XmlNode` / `dmidecode.XmlDoc` (`dmidecode.py`). -- C extension `xmlapi()` no longer returns Python libxml2 objects; it serializes libxml2 `xmlNode` to XML bytes and returns `bytes` to Python (`src/dmidecodemodule.c`). +- C extension `xmlapi()` no longer returns Python libxml2 objects; it serializes libxml2 `xmlNode` to XML bytes and returns `bytes` to Python (`src/dmidecodemodule.c`). `xmlapi()` accepts both positional and keyword arguments for compatibility. - Unit tests were updated to validate `dmidecode.XmlNode` / `dmidecode.XmlDoc` instead of `libxml2.xmlNode` / `libxml2.xmlDoc` (`unit-tests/unit`). Known gaps/blockers right now: @@ -35,7 +35,7 @@ The project currently uses libxml2 in several key areas: 2. **C Extension** (`src/dmidecodemodule.c`, `src/libxml_wrap.h`): - Uses libxml2 C API extensively - Creates XML documents and nodes using libxml2 functions - - `xmlapi()` returns serialized XML (`bytes`) instead of Python libxml2 wrapper objects + - `xmlapi()` returns serialized XML (`bytes`) instead of Python libxml2 wrapper objects. Input args can be positional or keywords (`query_type`, `result_type`, `section`, `typeid`). 3. **Testing** (`unit-tests/unit`): - Validates return types as `dmidecode.XmlNode` / `dmidecode.XmlDoc` diff --git a/dmidecode.py b/dmidecode.py index c3d91e0..06e0ec7 100644 --- a/dmidecode.py +++ b/dmidecode.py @@ -41,7 +41,8 @@ def __init__(self, element): def __getattr__(self, name): """Delegate attribute access to the underlying Element""" - return getattr(self.element, name) + element = object.__getattribute__(self, 'element') + return getattr(element, name) class XmlDoc: """ @@ -57,7 +58,8 @@ def getroot(self): def __getattr__(self, name): """Delegate attribute access to the underlying ElementTree""" - return getattr(self.element_tree, name) + element_tree = object.__getattribute__(self, 'element_tree') + return getattr(element_tree, name) class dmidecodeXML: "Native Python API for retrieving dmidecode information as XML" @@ -82,7 +84,7 @@ def SetResultType(self, type): def _create_xml_from_string(self, xml_string): """ Internal method to create XML objects from string representation - This will be used when the C extension returns XML as strings + This will be used when the C extension returns XML as bytes (or str) """ try: element = ET.fromstring(xml_string) @@ -114,4 +116,3 @@ def QueryTypeId(self, tpid): # Convert to appropriate XML object return self._create_xml_from_string(xml_string) - diff --git a/src/dmidecodemodule.c b/src/dmidecodemodule.c index 78a4ef7..2d8bd87 100644 --- a/src/dmidecodemodule.c +++ b/src/dmidecodemodule.c @@ -52,6 +52,15 @@ #include "dmidump.h" #include +/* + * PyMethodDef.ml_meth is typed as PyCFunction (2-arg), but METH_KEYWORDS + * functions take 3 args. CPython provides _PyCFunction_CAST() to silence + * cast-function-type warnings by casting via void(*)(void). + */ +#ifndef _PyCFunction_CAST +#define _PyCFunction_CAST(func) ((PyCFunction)(void(*)(void))(func)) +#endif + #if (PY_VERSION_HEX < 0x03030000) char *PyUnicode_AsUTF8(PyObject *unicode) { PyObject *as_bytes = PyUnicode_AsUTF8String(unicode); @@ -689,16 +698,68 @@ static PyObject *dmidecode_get_type(PyObject * self, PyObject * args) return pydata; } -static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args) +static int pyobj_parse_int(PyObject *obj, int *out) { + long v; + char *end = NULL; + + if (obj == NULL) { + return 0; + } + + if (PyLong_Check(obj)) { + v = PyLong_AsLong(obj); + if (PyErr_Occurred()) { + return -1; + } + *out = (int)v; + return 1; + } + + if (PyUnicode_Check(obj)) { + const char *s = PyUnicode_AsUTF8(obj); + if (s == NULL) { + return -1; + } + v = strtol(s, &end, 10); + if (end == s || *end != '\0') { + return 0; + } + *out = (int)v; + return 1; + } + + if (PyBytes_Check(obj)) { + char *s = PyBytes_AsString(obj); + if (s == NULL) { + return -1; + } + v = strtol(s, &end, 10); + if (end == s || *end != '\0') { + return 0; + } + *out = (int)v; + return 1; + } + + return 0; +} + +static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args, PyObject *kwds) +{ + (void)self; PyObject *pydata = NULL; xmlDoc *temp_doc = NULL; xmlNode *dmixml_n = NULL; xmlChar *xml_buffer = NULL; const char *sect_query = NULL, *qtype = NULL, *rtype = NULL; - PyObject *third_arg = NULL; + PyObject *section_arg = NULL; + PyObject *typeid_arg = NULL; int type_query = -1; int buffer_size = 0; + int parsed; + static char *kwlist[] = { (char *)"query_type", (char *)"result_type", + (char *)"section", (char *)"typeid", NULL }; // Parse arguments. // We support both of these call shapes: @@ -706,24 +767,31 @@ static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args) // xmlapi('t', rtype, typeid) // And the legacy 4-arg variant: // xmlapi('t', rtype, section_placeholder, typeid) - if( !PyArg_ParseTuple(args, "ss|Oi", &qtype, &rtype, &third_arg, &type_query) ) { + // As well as keyword args: + // xmlapi(query_type='s', result_type=rtype, section=section) + // xmlapi(query_type='t', result_type=rtype, typeid=typeid) + if( !PyArg_ParseTupleAndKeywords(args, kwds, "ss|OO", kwlist, + &qtype, &rtype, §ion_arg, &typeid_arg) ) { return NULL; } - if( third_arg == Py_None ) { - third_arg = NULL; + if( section_arg == Py_None ) { + section_arg = NULL; + } + if( typeid_arg == Py_None ) { + typeid_arg = NULL; } // Check for sensible arguments and retrieve the xmlNode with DMI data switch( *qtype ) { case 's': // Section / GroupName - if( third_arg == NULL ) { + if( section_arg == NULL ) { PyReturnError(PyExc_TypeError, "section argument cannot be NULL") } - if( PyUnicode_Check(third_arg) ) { - sect_query = PyUnicode_AsUTF8(third_arg); - } else if( PyBytes_Check(third_arg) ) { - sect_query = PyBytes_AsString(third_arg); + if( PyUnicode_Check(section_arg) ) { + sect_query = PyUnicode_AsUTF8(section_arg); + } else if( PyBytes_Check(section_arg) ) { + sect_query = PyBytes_AsString(section_arg); } else { PyReturnError(PyExc_TypeError, "section argument must be str or bytes") } @@ -735,27 +803,23 @@ static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args) break; case 't': // TypeID / direct TypeMap - // Prefer a positional typeid in the third slot. - if( third_arg != NULL ) { - if( PyLong_Check(third_arg) ) { - long v = PyLong_AsLong(third_arg); - if( PyErr_Occurred() ) { - return NULL; - } - type_query = (int) v; - } else if( type_query < 0 && (PyUnicode_Check(third_arg) || PyBytes_Check(third_arg)) ) { - // Backwards compatibility: allow typeid passed as string. - const char *s = PyUnicode_Check(third_arg) ? PyUnicode_AsUTF8(third_arg) - : PyBytes_AsString(third_arg); - if( s == NULL ) { - return NULL; - } - type_query = atoi(s); + // Prefer an explicit typeid= keyword / 4th positional arg. + parsed = pyobj_parse_int(typeid_arg, &type_query); + if( parsed < 0 ) { + return NULL; + } + + // Backwards compatibility: accept typeid passed in the 3rd slot. + if( parsed == 0 && section_arg != NULL ) { + parsed = pyobj_parse_int(section_arg, &type_query); + if( parsed < 0 ) { + return NULL; } } + if( type_query < 0 ) { PyReturnError(PyExc_TypeError, - "typeid keyword must be set and must be a positive integer"); + "typeid must be set and must be a positive integer"); } else if( type_query > 255 ) { PyReturnError(PyExc_ValueError, "typeid keyword must be an integer between 0 and 255"); @@ -805,6 +869,11 @@ static PyObject *dmidecode_xmlapi(PyObject *self, PyObject *args) return pydata; } +static PyObject *dmidecode_xmlapi_kw(PyObject *self, PyObject *args, PyObject *kwds) +{ + return dmidecode_xmlapi(self, args, kwds); +} + static PyObject *dmidecode_dump(PyObject * self, PyObject * null) @@ -959,8 +1028,8 @@ static PyMethodDef DMIDataMethods[] = { {(char *)"pythonmap", dmidecode_set_pythonxmlmap, METH_O, (char *) "Use another python dict map definition. The default file is " PYTHON_XML_MAP}, - {(char *)"xmlapi", dmidecode_xmlapi, METH_VARARGS, - (char *) "Internal API for retrieving data as raw XML data"}, + {(char *)"xmlapi", _PyCFunction_CAST(dmidecode_xmlapi_kw), METH_VARARGS | METH_KEYWORDS, + (char *) "Internal API for retrieving data as raw XML data"}, {(char *)"get_warnings", dmidecode_get_warnings, METH_NOARGS, diff --git a/unit-tests/unit b/unit-tests/unit index 576ad66..997dae1 100755 --- a/unit-tests/unit +++ b/unit-tests/unit @@ -8,11 +8,6 @@ if sys.version_info[0] < 3: import commands as subprocess from getopt import getopt -# Setup temporary sys.path() with our build dir -(sysname, nodename, release, version, machine) = os.uname() -pyver = sys.version[:3] -sys.path.insert(0,'../build/lib.%s-%s-%s' % (sysname.lower(), machine, pyver)) - root_user = (os.getuid() == 0 and True or False) ERROR = False