leptris 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- leptris-1.2.0/PKG-INFO +95 -0
- leptris-1.2.0/README.md +83 -0
- leptris-1.2.0/leptris/__init__.py +21 -0
- leptris-1.2.0/leptris/_ffi.py +109 -0
- leptris-1.2.0/leptris/document.py +79 -0
- leptris-1.2.0/leptris/element.py +109 -0
- leptris-1.2.0/leptris/error.py +5 -0
- leptris-1.2.0/leptris/node.py +84 -0
- leptris-1.2.0/leptris/xpath.py +42 -0
- leptris-1.2.0/leptris.egg-info/PKG-INFO +95 -0
- leptris-1.2.0/leptris.egg-info/SOURCES.txt +15 -0
- leptris-1.2.0/leptris.egg-info/dependency_links.txt +1 -0
- leptris-1.2.0/leptris.egg-info/requires.txt +4 -0
- leptris-1.2.0/leptris.egg-info/top_level.txt +1 -0
- leptris-1.2.0/pyproject.toml +19 -0
- leptris-1.2.0/setup.cfg +4 -0
- leptris-1.2.0/tests/test_binding.py +147 -0
leptris-1.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leptris
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Python bindings for libleptris — fast XML 1.0 parsing and XPath 1.0
|
|
5
|
+
Author: Ribose
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: cffi
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest; extra == "test"
|
|
12
|
+
|
|
13
|
+
# leptris (Python) — bindings for libleptris
|
|
14
|
+
|
|
15
|
+
`leptris` wraps the [libleptris](https://github.com/leptris/leptris)
|
|
16
|
+
C API (XML 1.0 parsing, XPath 1.0) using `cffi` in ABI mode — the
|
|
17
|
+
`cdef` in `leptris/_ffi.py` mirrors libleptris's public headers.
|
|
18
|
+
|
|
19
|
+
The pinned libleptris version lives in `libleptris-version.txt`
|
|
20
|
+
(lockstep releases); CI builds it from the release tarball. The
|
|
21
|
+
binding loads the shared library from `LEPTRIS_LIB_PATH` or the
|
|
22
|
+
loader path.
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Python 3.8+
|
|
27
|
+
- `cffi` (`pip install cffi`)
|
|
28
|
+
- libleptris as a shared library (`libleptris.dylib` / `libleptris.so`)
|
|
29
|
+
on the loader path, or pointed to by `LEPTRIS_LIB_PATH`. For a
|
|
30
|
+
development checkout:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cmake -B build -S . -DLEPTRIS_BUILD_SHARED=ON
|
|
34
|
+
cmake --build build --target leptris_shared
|
|
35
|
+
export LEPTRIS_LIB_PATH=$PWD/build/src/libleptris.dylib
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from leptris import Document
|
|
42
|
+
|
|
43
|
+
doc = Document.parse("<library><book id='1'>Ulysses</book></library>")
|
|
44
|
+
|
|
45
|
+
doc.root.name # "library"
|
|
46
|
+
book = doc.root.first_child_element
|
|
47
|
+
book.name # "book"
|
|
48
|
+
book.attribute("id") # "1"
|
|
49
|
+
book.text # "Ulysses"
|
|
50
|
+
|
|
51
|
+
doc.xpath("count(//book)") # 1.0
|
|
52
|
+
[e.text for e in doc.xpath("//book")] # ["Ulysses"]
|
|
53
|
+
|
|
54
|
+
doc.close() # or: with Document.parse(xml) as doc: ...
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Layout
|
|
58
|
+
|
|
59
|
+
- `leptris/_ffi.py` — cdef + shared-library loading (single source
|
|
60
|
+
of the C surface, mirroring the Ruby binding's `lib/leptris.rb`)
|
|
61
|
+
- `leptris/document.py`, `element.py`, `node.py`, `xpath.py`,
|
|
62
|
+
`error.py` — typed wrappers
|
|
63
|
+
- `tests/` — pytest suite (run: `pytest` with `LEPTRIS_LIB_PATH` set)
|
|
64
|
+
|
|
65
|
+
## Memory model
|
|
66
|
+
|
|
67
|
+
The `Document` owns the whole tree and its pool. Accessor strings
|
|
68
|
+
are copied into Python `str` at the boundary, so nothing depends on
|
|
69
|
+
document lifetime after a call returns. Elements keep a reference to
|
|
70
|
+
their `Document`, so the pool cannot be freed while any wrapper is
|
|
71
|
+
alive. Prefer explicit `close()` / the context manager; `__del__` is
|
|
72
|
+
a refcounting safety net, not a contract.
|
|
73
|
+
|
|
74
|
+
## Versioning
|
|
75
|
+
|
|
76
|
+
The package version tracks libleptris (lockstep): library 1.1.0 ↔
|
|
77
|
+
leptris 1.1.0.
|
|
78
|
+
|
|
79
|
+
## Publishing
|
|
80
|
+
|
|
81
|
+
Releases publish to PyPI via `.github/workflows/release.yml`,
|
|
82
|
+
using PyPI **trusted publishing** (no stored credentials). The
|
|
83
|
+
workflow runs on manual dispatch (ships the version in
|
|
84
|
+
`pyproject.toml`) and is called by the libleptris release flow
|
|
85
|
+
(`publish: true`), so every libleptris release ships the wheel.
|
|
86
|
+
|
|
87
|
+
## Local development
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python3 -m venv .venv
|
|
91
|
+
./.venv/bin/pip install --upgrade build setuptools wheel pytest cffi
|
|
92
|
+
./.venv/bin/python -m build
|
|
93
|
+
LEPTRIS_LIB_PATH=../../build-shared/src/libleptris.dylib \
|
|
94
|
+
./.venv/bin/python -m pytest tests/ -q
|
|
95
|
+
```
|
leptris-1.2.0/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# leptris (Python) — bindings for libleptris
|
|
2
|
+
|
|
3
|
+
`leptris` wraps the [libleptris](https://github.com/leptris/leptris)
|
|
4
|
+
C API (XML 1.0 parsing, XPath 1.0) using `cffi` in ABI mode — the
|
|
5
|
+
`cdef` in `leptris/_ffi.py` mirrors libleptris's public headers.
|
|
6
|
+
|
|
7
|
+
The pinned libleptris version lives in `libleptris-version.txt`
|
|
8
|
+
(lockstep releases); CI builds it from the release tarball. The
|
|
9
|
+
binding loads the shared library from `LEPTRIS_LIB_PATH` or the
|
|
10
|
+
loader path.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- Python 3.8+
|
|
15
|
+
- `cffi` (`pip install cffi`)
|
|
16
|
+
- libleptris as a shared library (`libleptris.dylib` / `libleptris.so`)
|
|
17
|
+
on the loader path, or pointed to by `LEPTRIS_LIB_PATH`. For a
|
|
18
|
+
development checkout:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cmake -B build -S . -DLEPTRIS_BUILD_SHARED=ON
|
|
22
|
+
cmake --build build --target leptris_shared
|
|
23
|
+
export LEPTRIS_LIB_PATH=$PWD/build/src/libleptris.dylib
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from leptris import Document
|
|
30
|
+
|
|
31
|
+
doc = Document.parse("<library><book id='1'>Ulysses</book></library>")
|
|
32
|
+
|
|
33
|
+
doc.root.name # "library"
|
|
34
|
+
book = doc.root.first_child_element
|
|
35
|
+
book.name # "book"
|
|
36
|
+
book.attribute("id") # "1"
|
|
37
|
+
book.text # "Ulysses"
|
|
38
|
+
|
|
39
|
+
doc.xpath("count(//book)") # 1.0
|
|
40
|
+
[e.text for e in doc.xpath("//book")] # ["Ulysses"]
|
|
41
|
+
|
|
42
|
+
doc.close() # or: with Document.parse(xml) as doc: ...
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Layout
|
|
46
|
+
|
|
47
|
+
- `leptris/_ffi.py` — cdef + shared-library loading (single source
|
|
48
|
+
of the C surface, mirroring the Ruby binding's `lib/leptris.rb`)
|
|
49
|
+
- `leptris/document.py`, `element.py`, `node.py`, `xpath.py`,
|
|
50
|
+
`error.py` — typed wrappers
|
|
51
|
+
- `tests/` — pytest suite (run: `pytest` with `LEPTRIS_LIB_PATH` set)
|
|
52
|
+
|
|
53
|
+
## Memory model
|
|
54
|
+
|
|
55
|
+
The `Document` owns the whole tree and its pool. Accessor strings
|
|
56
|
+
are copied into Python `str` at the boundary, so nothing depends on
|
|
57
|
+
document lifetime after a call returns. Elements keep a reference to
|
|
58
|
+
their `Document`, so the pool cannot be freed while any wrapper is
|
|
59
|
+
alive. Prefer explicit `close()` / the context manager; `__del__` is
|
|
60
|
+
a refcounting safety net, not a contract.
|
|
61
|
+
|
|
62
|
+
## Versioning
|
|
63
|
+
|
|
64
|
+
The package version tracks libleptris (lockstep): library 1.1.0 ↔
|
|
65
|
+
leptris 1.1.0.
|
|
66
|
+
|
|
67
|
+
## Publishing
|
|
68
|
+
|
|
69
|
+
Releases publish to PyPI via `.github/workflows/release.yml`,
|
|
70
|
+
using PyPI **trusted publishing** (no stored credentials). The
|
|
71
|
+
workflow runs on manual dispatch (ships the version in
|
|
72
|
+
`pyproject.toml`) and is called by the libleptris release flow
|
|
73
|
+
(`publish: true`), so every libleptris release ships the wheel.
|
|
74
|
+
|
|
75
|
+
## Local development
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python3 -m venv .venv
|
|
79
|
+
./.venv/bin/pip install --upgrade build setuptools wheel pytest cffi
|
|
80
|
+
./.venv/bin/python -m build
|
|
81
|
+
LEPTRIS_LIB_PATH=../../build-shared/src/libleptris.dylib \
|
|
82
|
+
./.venv/bin/python -m pytest tests/ -q
|
|
83
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""leptris — Python bindings for libleptris.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
|
|
5
|
+
from leptris import Document
|
|
6
|
+
|
|
7
|
+
doc = Document.parse("<root><item>hi</item></root>")
|
|
8
|
+
print(doc.root.name)
|
|
9
|
+
|
|
10
|
+
Requires libleptris on the library search path (or LEPTRIS_LIB_PATH).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "1.2.0"
|
|
14
|
+
|
|
15
|
+
from .document import Document
|
|
16
|
+
from .element import Element
|
|
17
|
+
from .error import LeptrisError
|
|
18
|
+
from .node import Node
|
|
19
|
+
from .xpath import XPath
|
|
20
|
+
|
|
21
|
+
__all__ = ["Document", "Element", "Node", "XPath", "LeptrisError"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""C bridge for leptris.
|
|
2
|
+
|
|
3
|
+
cffi ABI mode: the cdef below mirrors the public headers
|
|
4
|
+
(src/include/leptris/). All handles are opaque pointers; strings
|
|
5
|
+
returned by accessors are document-owned and only valid until
|
|
6
|
+
leptris_document_free — copy into Python str at the boundary.
|
|
7
|
+
|
|
8
|
+
The library is resolved from LEPTRIS_LIB_PATH, then the usual
|
|
9
|
+
install names, then the local build directory.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from cffi import FFI
|
|
15
|
+
|
|
16
|
+
ffi = FFI()
|
|
17
|
+
|
|
18
|
+
ffi.cdef(
|
|
19
|
+
"""
|
|
20
|
+
typedef struct leptris_document* LeptrisDocument;
|
|
21
|
+
typedef struct leptris_element* LeptrisElement;
|
|
22
|
+
typedef struct leptris_node* LeptrisNodeRef;
|
|
23
|
+
typedef struct leptris_attribute* LeptrisAttribute;
|
|
24
|
+
typedef struct leptris_xpath_result* LeptrisXPathResult;
|
|
25
|
+
|
|
26
|
+
LeptrisDocument leptris_parse_string(const char* xml, size_t len, int* status);
|
|
27
|
+
void leptris_document_free(LeptrisDocument doc);
|
|
28
|
+
LeptrisElement leptris_document_root(LeptrisDocument doc);
|
|
29
|
+
char* leptris_document_serialize(LeptrisDocument doc, void* options);
|
|
30
|
+
int leptris_xinclude_process(LeptrisDocument doc, const char* base_path);
|
|
31
|
+
|
|
32
|
+
int leptris_node_get_type(LeptrisNodeRef node);
|
|
33
|
+
LeptrisNodeRef leptris_node_first_child(LeptrisNodeRef node);
|
|
34
|
+
LeptrisNodeRef leptris_node_next_sibling(LeptrisNodeRef node);
|
|
35
|
+
LeptrisNodeRef leptris_node_previous_sibling(LeptrisNodeRef node);
|
|
36
|
+
size_t leptris_node_child_count(LeptrisNodeRef node);
|
|
37
|
+
LeptrisElement leptris_node_as_element(LeptrisNodeRef node);
|
|
38
|
+
LeptrisNodeRef leptris_element_as_node(LeptrisElement elem);
|
|
39
|
+
|
|
40
|
+
const char* leptris_element_name(LeptrisElement elem);
|
|
41
|
+
const char* leptris_element_text(LeptrisElement elem);
|
|
42
|
+
LeptrisElement leptris_element_first_child_any(LeptrisElement elem);
|
|
43
|
+
LeptrisElement leptris_element_parent(LeptrisElement elem);
|
|
44
|
+
const char* leptris_element_attribute(LeptrisElement elem,
|
|
45
|
+
const char* name);
|
|
46
|
+
LeptrisElement leptris_element_next_sibling_any(LeptrisElement elem);
|
|
47
|
+
LeptrisAttribute leptris_element_first_attribute(LeptrisElement elem);
|
|
48
|
+
LeptrisAttribute leptris_attribute_next(LeptrisAttribute attr);
|
|
49
|
+
const char* leptris_attribute_get_name(LeptrisAttribute attr);
|
|
50
|
+
const char* leptris_attribute_get_value(LeptrisElement elem,
|
|
51
|
+
LeptrisAttribute attr);
|
|
52
|
+
size_t leptris_element_attribute_count(LeptrisElement elem);
|
|
53
|
+
size_t leptris_element_child_count(LeptrisElement elem);
|
|
54
|
+
|
|
55
|
+
const char* leptris_text_node_get_content(LeptrisNodeRef node);
|
|
56
|
+
const char* leptris_comment_node_get_content(LeptrisNodeRef node);
|
|
57
|
+
const char* leptris_cdata_node_get_content(LeptrisNodeRef node);
|
|
58
|
+
const char* leptris_pi_node_get_target(LeptrisNodeRef node);
|
|
59
|
+
const char* leptris_pi_node_get_data(LeptrisNodeRef node);
|
|
60
|
+
|
|
61
|
+
LeptrisXPathResult leptris_xpath_eval(LeptrisDocument doc,
|
|
62
|
+
LeptrisElement context,
|
|
63
|
+
const char* expression);
|
|
64
|
+
void leptris_xpath_result_free(LeptrisXPathResult result);
|
|
65
|
+
int leptris_xpath_result_type(LeptrisXPathResult result);
|
|
66
|
+
double leptris_xpath_result_number(LeptrisXPathResult result);
|
|
67
|
+
int leptris_xpath_result_boolean(LeptrisXPathResult result);
|
|
68
|
+
char* leptris_xpath_result_string(LeptrisXPathResult result);
|
|
69
|
+
size_t leptris_xpath_result_count(LeptrisXPathResult result);
|
|
70
|
+
LeptrisElement leptris_xpath_result_get(LeptrisXPathResult result, size_t index);
|
|
71
|
+
|
|
72
|
+
void leptris_free_string(char* str);
|
|
73
|
+
"""
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _load():
|
|
78
|
+
candidates = []
|
|
79
|
+
if os.environ.get("LEPTRIS_LIB_PATH"):
|
|
80
|
+
candidates.append(os.environ["LEPTRIS_LIB_PATH"])
|
|
81
|
+
candidates += ["libleptris.dylib", "libleptris.so", "leptris.dll"]
|
|
82
|
+
here = os.path.dirname(__file__)
|
|
83
|
+
candidates += [
|
|
84
|
+
os.path.join(here, "..", "..", "..", "build", "src", "libleptris.dylib"),
|
|
85
|
+
os.path.join(here, "..", "..", "..", "build", "src", "libleptris.so"),
|
|
86
|
+
]
|
|
87
|
+
for name in candidates:
|
|
88
|
+
try:
|
|
89
|
+
return ffi.dlopen(name)
|
|
90
|
+
except OSError:
|
|
91
|
+
continue
|
|
92
|
+
raise ImportError(
|
|
93
|
+
"libleptris not found; build it or set LEPTRIS_LIB_PATH"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
lib = _load()
|
|
98
|
+
|
|
99
|
+
NODE_ELEMENT = 0
|
|
100
|
+
NODE_TEXT = 1
|
|
101
|
+
NODE_COMMENT = 2
|
|
102
|
+
NODE_CDATA = 3
|
|
103
|
+
NODE_PI = 4
|
|
104
|
+
NODE_DOCTYPE = 5
|
|
105
|
+
|
|
106
|
+
XPATH_NODESET = 0
|
|
107
|
+
XPATH_BOOLEAN = 1
|
|
108
|
+
XPATH_NUMBER = 2
|
|
109
|
+
XPATH_STRING = 3
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Document — wraps LeptrisDocument.
|
|
2
|
+
|
|
3
|
+
The document owns the entire DOM tree and memory pool. Release it
|
|
4
|
+
with close() (or the context manager); __del__ is a last-resort
|
|
5
|
+
safety net for CPython refcounting, not a contract.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from . import _ffi
|
|
9
|
+
from .error import LeptrisError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Document:
|
|
13
|
+
def __init__(self, _ptr):
|
|
14
|
+
self._ptr = _ptr
|
|
15
|
+
self._freed = False
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def parse(cls, xml):
|
|
19
|
+
if isinstance(xml, str):
|
|
20
|
+
xml = xml.encode("utf-8")
|
|
21
|
+
if not isinstance(xml, (bytes, bytearray, memoryview)):
|
|
22
|
+
raise TypeError("xml must be str or bytes")
|
|
23
|
+
xml = bytes(xml)
|
|
24
|
+
status = _ffi.ffi.new("int*")
|
|
25
|
+
ptr = _ffi.lib.leptris_parse_string(xml, len(xml), status)
|
|
26
|
+
if ptr == _ffi.ffi.NULL:
|
|
27
|
+
raise LeptrisError(f"parse failed (status={status[0]})")
|
|
28
|
+
return cls(ptr)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def root(self):
|
|
32
|
+
ptr = _ffi.lib.leptris_document_root(self._ptr)
|
|
33
|
+
if ptr == _ffi.ffi.NULL:
|
|
34
|
+
return None
|
|
35
|
+
from .element import Element
|
|
36
|
+
|
|
37
|
+
return Element(ptr, self)
|
|
38
|
+
|
|
39
|
+
def serialize(self) -> str:
|
|
40
|
+
result = _ffi.lib.leptris_document_serialize(self._ptr, _ffi.ffi.NULL)
|
|
41
|
+
if result == _ffi.ffi.NULL:
|
|
42
|
+
return ""
|
|
43
|
+
value = _ffi.ffi.string(result).decode("utf-8")
|
|
44
|
+
_ffi.lib.leptris_free_string(result)
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
def process_xinclude(self, base_url=None):
|
|
48
|
+
base = base_url.encode("utf-8") if base_url is not None else _ffi.ffi.NULL
|
|
49
|
+
rc = _ffi.lib.leptris_xinclude_process(self._ptr, base)
|
|
50
|
+
if rc != 0:
|
|
51
|
+
raise LeptrisError("XInclude processing failed")
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def xpath(self, expression, context=None):
|
|
55
|
+
from .xpath import XPath
|
|
56
|
+
|
|
57
|
+
return XPath.evaluate(self, context, expression)
|
|
58
|
+
|
|
59
|
+
def close(self):
|
|
60
|
+
if not self._freed:
|
|
61
|
+
_ffi.lib.leptris_document_free(self._ptr)
|
|
62
|
+
self._freed = True
|
|
63
|
+
self._ptr = _ffi.ffi.NULL
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def closed(self) -> bool:
|
|
67
|
+
return self._freed
|
|
68
|
+
|
|
69
|
+
def __enter__(self):
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
73
|
+
self.close()
|
|
74
|
+
|
|
75
|
+
def __del__(self):
|
|
76
|
+
try:
|
|
77
|
+
self.close()
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Element — wraps LeptrisElement.
|
|
2
|
+
|
|
3
|
+
Elements are owned by their parent Document; they are never freed
|
|
4
|
+
directly. Element objects keep a reference to the Document so the
|
|
5
|
+
tree cannot outlive its pool.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from . import _ffi
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Element:
|
|
12
|
+
def __init__(self, _ptr, document):
|
|
13
|
+
self._ptr = _ptr
|
|
14
|
+
self._document = document
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def document(self):
|
|
18
|
+
return self._document
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self) -> str:
|
|
22
|
+
value = _ffi.lib.leptris_element_name(self._ptr)
|
|
23
|
+
return _ffi.ffi.string(value).decode("utf-8") if value != _ffi.ffi.NULL else ""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def text(self) -> str:
|
|
27
|
+
value = _ffi.lib.leptris_element_text(self._ptr)
|
|
28
|
+
return _ffi.ffi.string(value).decode("utf-8") if value != _ffi.ffi.NULL else ""
|
|
29
|
+
|
|
30
|
+
def attribute(self, name: str, default=None):
|
|
31
|
+
value = _ffi.lib.leptris_element_attribute(
|
|
32
|
+
self._ptr, name.encode("utf-8")
|
|
33
|
+
)
|
|
34
|
+
if value == _ffi.ffi.NULL:
|
|
35
|
+
return default
|
|
36
|
+
return _ffi.ffi.string(value).decode("utf-8")
|
|
37
|
+
|
|
38
|
+
__getitem__ = attribute
|
|
39
|
+
|
|
40
|
+
def attributes(self):
|
|
41
|
+
"""Yield (name, value) for every attribute in document order.
|
|
42
|
+
|
|
43
|
+
Handle-based iteration — O(n) total where index-based access
|
|
44
|
+
re-walks the list per call.
|
|
45
|
+
"""
|
|
46
|
+
attr = _ffi.lib.leptris_element_first_attribute(self._ptr)
|
|
47
|
+
while attr != _ffi.ffi.NULL:
|
|
48
|
+
name = _ffi.ffi.string(
|
|
49
|
+
_ffi.lib.leptris_attribute_get_name(attr)
|
|
50
|
+
).decode("utf-8")
|
|
51
|
+
value = _ffi.ffi.string(
|
|
52
|
+
_ffi.lib.leptris_attribute_get_value(self._ptr, attr)
|
|
53
|
+
).decode("utf-8")
|
|
54
|
+
yield (name, value)
|
|
55
|
+
attr = _ffi.lib.leptris_attribute_next(attr)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def attribute_count(self) -> int:
|
|
59
|
+
return _ffi.lib.leptris_element_attribute_count(self._ptr)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def child_count(self) -> int:
|
|
63
|
+
return _ffi.lib.leptris_element_child_count(self._ptr)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def parent(self):
|
|
67
|
+
ptr = _ffi.lib.leptris_element_parent(self._ptr)
|
|
68
|
+
if ptr == _ffi.ffi.NULL:
|
|
69
|
+
return None
|
|
70
|
+
return Element(ptr, self._document)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def first_child_element(self):
|
|
74
|
+
ptr = _ffi.lib.leptris_element_first_child_any(self._ptr)
|
|
75
|
+
if ptr == _ffi.ffi.NULL:
|
|
76
|
+
return None
|
|
77
|
+
return Element(ptr, self._document)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def next_sibling_element(self):
|
|
81
|
+
# The node-level sibling chain interleaves text nodes, so
|
|
82
|
+
# walk until the next element (or the end of the chain).
|
|
83
|
+
node = _ffi.lib.leptris_node_next_sibling(_ffi.lib.leptris_element_as_node(self._ptr))
|
|
84
|
+
while node != _ffi.ffi.NULL:
|
|
85
|
+
elem = _ffi.lib.leptris_node_as_element(node)
|
|
86
|
+
if elem != _ffi.ffi.NULL:
|
|
87
|
+
return Element(elem, self._document)
|
|
88
|
+
node = _ffi.lib.leptris_node_next_sibling(node)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def child_elements(self):
|
|
92
|
+
child = self.first_child_element
|
|
93
|
+
while child is not None:
|
|
94
|
+
yield child
|
|
95
|
+
child = child.next_sibling_element
|
|
96
|
+
|
|
97
|
+
def to_node(self):
|
|
98
|
+
from .node import Node
|
|
99
|
+
|
|
100
|
+
return Node(_ffi.lib.leptris_element_as_node(self._ptr), self._document)
|
|
101
|
+
|
|
102
|
+
def xpath(self, expression):
|
|
103
|
+
return self._document.xpath(expression, context=self)
|
|
104
|
+
|
|
105
|
+
def __iter__(self):
|
|
106
|
+
return self.child_elements()
|
|
107
|
+
|
|
108
|
+
def __repr__(self):
|
|
109
|
+
return f"<leptris.Element {self.name!r}>"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Node — wraps LeptrisNodeRef for generic tree traversal.
|
|
2
|
+
|
|
3
|
+
Covers every node type (element, text, comment, CDATA, PI, doctype);
|
|
4
|
+
Element is the typed view for element nodes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from . import _ffi
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Node:
|
|
11
|
+
def __init__(self, _ptr, document):
|
|
12
|
+
self._ptr = _ptr
|
|
13
|
+
self._document = document
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def type(self) -> int:
|
|
17
|
+
return _ffi.lib.leptris_node_get_type(self._ptr)
|
|
18
|
+
|
|
19
|
+
def is_element(self) -> bool:
|
|
20
|
+
return self.type == _ffi.NODE_ELEMENT
|
|
21
|
+
|
|
22
|
+
def is_text(self) -> bool:
|
|
23
|
+
return self.type == _ffi.NODE_TEXT
|
|
24
|
+
|
|
25
|
+
def is_comment(self) -> bool:
|
|
26
|
+
return self.type == _ffi.NODE_COMMENT
|
|
27
|
+
|
|
28
|
+
def is_cdata(self) -> bool:
|
|
29
|
+
return self.type == _ffi.NODE_CDATA
|
|
30
|
+
|
|
31
|
+
def is_pi(self) -> bool:
|
|
32
|
+
return self.type == _ffi.NODE_PI
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def content(self):
|
|
36
|
+
t = self.type
|
|
37
|
+
if t == _ffi.NODE_TEXT:
|
|
38
|
+
getter = _ffi.lib.leptris_text_node_get_content
|
|
39
|
+
elif t == _ffi.NODE_COMMENT:
|
|
40
|
+
getter = _ffi.lib.leptris_comment_node_get_content
|
|
41
|
+
elif t == _ffi.NODE_CDATA:
|
|
42
|
+
getter = _ffi.lib.leptris_cdata_node_get_content
|
|
43
|
+
else:
|
|
44
|
+
return None
|
|
45
|
+
value = getter(self._ptr)
|
|
46
|
+
return _ffi.ffi.string(value).decode("utf-8") if value != _ffi.ffi.NULL else ""
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def first_child(self):
|
|
50
|
+
ptr = _ffi.lib.leptris_node_first_child(self._ptr)
|
|
51
|
+
if ptr == _ffi.ffi.NULL:
|
|
52
|
+
return None
|
|
53
|
+
return Node(ptr, self._document)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def next_sibling(self):
|
|
57
|
+
ptr = _ffi.lib.leptris_node_next_sibling(self._ptr)
|
|
58
|
+
if ptr == _ffi.ffi.NULL:
|
|
59
|
+
return None
|
|
60
|
+
return Node(ptr, self._document)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def previous_sibling(self):
|
|
64
|
+
ptr = _ffi.lib.leptris_node_previous_sibling(self._ptr)
|
|
65
|
+
if ptr == _ffi.ffi.NULL:
|
|
66
|
+
return None
|
|
67
|
+
return Node(ptr, self._document)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def child_count(self) -> int:
|
|
71
|
+
return _ffi.lib.leptris_node_child_count(self._ptr)
|
|
72
|
+
|
|
73
|
+
def as_element(self):
|
|
74
|
+
if not self.is_element():
|
|
75
|
+
return None
|
|
76
|
+
ptr = _ffi.lib.leptris_node_as_element(self._ptr)
|
|
77
|
+
if ptr == _ffi.ffi.NULL:
|
|
78
|
+
return None
|
|
79
|
+
from .element import Element
|
|
80
|
+
|
|
81
|
+
return Element(ptr, self._document)
|
|
82
|
+
|
|
83
|
+
def __repr__(self):
|
|
84
|
+
return f"<leptris.Node type={self.type}>"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""XPath — evaluates XPath 1.0 expressions.
|
|
2
|
+
|
|
3
|
+
Results are typed: nodeset results yield Element lists; scalar
|
|
4
|
+
results convert to native Python types.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from . import _ffi
|
|
8
|
+
from .error import LeptrisError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class XPath:
|
|
12
|
+
@staticmethod
|
|
13
|
+
def evaluate(document, context_element, expression):
|
|
14
|
+
ctx = context_element._ptr if context_element is not None else _ffi.ffi.NULL
|
|
15
|
+
result = _ffi.lib.leptris_xpath_eval(document._ptr, ctx, expression.encode("utf-8"))
|
|
16
|
+
if result == _ffi.ffi.NULL:
|
|
17
|
+
raise LeptrisError(f"XPath evaluation failed: {expression!r}")
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
result_type = _ffi.lib.leptris_xpath_result_type(result)
|
|
21
|
+
if result_type == _ffi.XPATH_NODESET:
|
|
22
|
+
count = _ffi.lib.leptris_xpath_result_count(result)
|
|
23
|
+
from .element import Element
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
Element(_ffi.lib.leptris_xpath_result_get(result, i), document)
|
|
27
|
+
for i in range(count)
|
|
28
|
+
]
|
|
29
|
+
if result_type == _ffi.XPATH_NUMBER:
|
|
30
|
+
return _ffi.lib.leptris_xpath_result_number(result)
|
|
31
|
+
if result_type == _ffi.XPATH_STRING:
|
|
32
|
+
ptr = _ffi.lib.leptris_xpath_result_string(result)
|
|
33
|
+
if ptr == _ffi.ffi.NULL:
|
|
34
|
+
return ""
|
|
35
|
+
value = _ffi.ffi.string(ptr).decode("utf-8")
|
|
36
|
+
_ffi.lib.leptris_free_string(ptr)
|
|
37
|
+
return value
|
|
38
|
+
if result_type == _ffi.XPATH_BOOLEAN:
|
|
39
|
+
return bool(_ffi.lib.leptris_xpath_result_boolean(result))
|
|
40
|
+
return None
|
|
41
|
+
finally:
|
|
42
|
+
_ffi.lib.leptris_xpath_result_free(result)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: leptris
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Python bindings for libleptris — fast XML 1.0 parsing and XPath 1.0
|
|
5
|
+
Author: Ribose
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: cffi
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest; extra == "test"
|
|
12
|
+
|
|
13
|
+
# leptris (Python) — bindings for libleptris
|
|
14
|
+
|
|
15
|
+
`leptris` wraps the [libleptris](https://github.com/leptris/leptris)
|
|
16
|
+
C API (XML 1.0 parsing, XPath 1.0) using `cffi` in ABI mode — the
|
|
17
|
+
`cdef` in `leptris/_ffi.py` mirrors libleptris's public headers.
|
|
18
|
+
|
|
19
|
+
The pinned libleptris version lives in `libleptris-version.txt`
|
|
20
|
+
(lockstep releases); CI builds it from the release tarball. The
|
|
21
|
+
binding loads the shared library from `LEPTRIS_LIB_PATH` or the
|
|
22
|
+
loader path.
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Python 3.8+
|
|
27
|
+
- `cffi` (`pip install cffi`)
|
|
28
|
+
- libleptris as a shared library (`libleptris.dylib` / `libleptris.so`)
|
|
29
|
+
on the loader path, or pointed to by `LEPTRIS_LIB_PATH`. For a
|
|
30
|
+
development checkout:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cmake -B build -S . -DLEPTRIS_BUILD_SHARED=ON
|
|
34
|
+
cmake --build build --target leptris_shared
|
|
35
|
+
export LEPTRIS_LIB_PATH=$PWD/build/src/libleptris.dylib
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from leptris import Document
|
|
42
|
+
|
|
43
|
+
doc = Document.parse("<library><book id='1'>Ulysses</book></library>")
|
|
44
|
+
|
|
45
|
+
doc.root.name # "library"
|
|
46
|
+
book = doc.root.first_child_element
|
|
47
|
+
book.name # "book"
|
|
48
|
+
book.attribute("id") # "1"
|
|
49
|
+
book.text # "Ulysses"
|
|
50
|
+
|
|
51
|
+
doc.xpath("count(//book)") # 1.0
|
|
52
|
+
[e.text for e in doc.xpath("//book")] # ["Ulysses"]
|
|
53
|
+
|
|
54
|
+
doc.close() # or: with Document.parse(xml) as doc: ...
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Layout
|
|
58
|
+
|
|
59
|
+
- `leptris/_ffi.py` — cdef + shared-library loading (single source
|
|
60
|
+
of the C surface, mirroring the Ruby binding's `lib/leptris.rb`)
|
|
61
|
+
- `leptris/document.py`, `element.py`, `node.py`, `xpath.py`,
|
|
62
|
+
`error.py` — typed wrappers
|
|
63
|
+
- `tests/` — pytest suite (run: `pytest` with `LEPTRIS_LIB_PATH` set)
|
|
64
|
+
|
|
65
|
+
## Memory model
|
|
66
|
+
|
|
67
|
+
The `Document` owns the whole tree and its pool. Accessor strings
|
|
68
|
+
are copied into Python `str` at the boundary, so nothing depends on
|
|
69
|
+
document lifetime after a call returns. Elements keep a reference to
|
|
70
|
+
their `Document`, so the pool cannot be freed while any wrapper is
|
|
71
|
+
alive. Prefer explicit `close()` / the context manager; `__del__` is
|
|
72
|
+
a refcounting safety net, not a contract.
|
|
73
|
+
|
|
74
|
+
## Versioning
|
|
75
|
+
|
|
76
|
+
The package version tracks libleptris (lockstep): library 1.1.0 ↔
|
|
77
|
+
leptris 1.1.0.
|
|
78
|
+
|
|
79
|
+
## Publishing
|
|
80
|
+
|
|
81
|
+
Releases publish to PyPI via `.github/workflows/release.yml`,
|
|
82
|
+
using PyPI **trusted publishing** (no stored credentials). The
|
|
83
|
+
workflow runs on manual dispatch (ships the version in
|
|
84
|
+
`pyproject.toml`) and is called by the libleptris release flow
|
|
85
|
+
(`publish: true`), so every libleptris release ships the wheel.
|
|
86
|
+
|
|
87
|
+
## Local development
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python3 -m venv .venv
|
|
91
|
+
./.venv/bin/pip install --upgrade build setuptools wheel pytest cffi
|
|
92
|
+
./.venv/bin/python -m build
|
|
93
|
+
LEPTRIS_LIB_PATH=../../build-shared/src/libleptris.dylib \
|
|
94
|
+
./.venv/bin/python -m pytest tests/ -q
|
|
95
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
leptris/__init__.py
|
|
4
|
+
leptris/_ffi.py
|
|
5
|
+
leptris/document.py
|
|
6
|
+
leptris/element.py
|
|
7
|
+
leptris/error.py
|
|
8
|
+
leptris/node.py
|
|
9
|
+
leptris/xpath.py
|
|
10
|
+
leptris.egg-info/PKG-INFO
|
|
11
|
+
leptris.egg-info/SOURCES.txt
|
|
12
|
+
leptris.egg-info/dependency_links.txt
|
|
13
|
+
leptris.egg-info/requires.txt
|
|
14
|
+
leptris.egg-info/top_level.txt
|
|
15
|
+
tests/test_binding.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
leptris
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "cffi"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "leptris"
|
|
7
|
+
version = "1.2.0"
|
|
8
|
+
description = "Python bindings for libleptris — fast XML 1.0 parsing and XPath 1.0"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Ribose" }]
|
|
13
|
+
dependencies = ["cffi"]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
test = ["pytest"]
|
|
17
|
+
|
|
18
|
+
[tool.setuptools]
|
|
19
|
+
packages = ["leptris"]
|
leptris-1.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from leptris import Document, LeptrisError
|
|
4
|
+
|
|
5
|
+
XML = """<?xml version="1.0"?>
|
|
6
|
+
<library>
|
|
7
|
+
<book id="1" lang="en">Ulysses</book>
|
|
8
|
+
<book id="2" lang="fr">L'Etranger</book>
|
|
9
|
+
<!-- a comment -->
|
|
10
|
+
</library>"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture()
|
|
14
|
+
def doc():
|
|
15
|
+
with Document.parse(XML) as doc:
|
|
16
|
+
yield doc
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestParse:
|
|
20
|
+
def test_root_name(self, doc):
|
|
21
|
+
assert doc.root.name == "library"
|
|
22
|
+
|
|
23
|
+
def test_root_is_element(self, doc):
|
|
24
|
+
assert doc.root.to_node().is_element()
|
|
25
|
+
|
|
26
|
+
def test_parse_error_raises(self):
|
|
27
|
+
with pytest.raises(LeptrisError):
|
|
28
|
+
Document.parse("<unclosed>")
|
|
29
|
+
|
|
30
|
+
def test_bytes_input(self):
|
|
31
|
+
with Document.parse(b"<r/>") as doc:
|
|
32
|
+
assert doc.root.name == "r"
|
|
33
|
+
|
|
34
|
+
def test_type_error_on_non_string(self):
|
|
35
|
+
with pytest.raises(TypeError):
|
|
36
|
+
Document.parse(123)
|
|
37
|
+
|
|
38
|
+
def test_close_is_idempotent(self):
|
|
39
|
+
doc = Document.parse("<r/>")
|
|
40
|
+
doc.close()
|
|
41
|
+
doc.close()
|
|
42
|
+
assert doc.closed
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestElement:
|
|
46
|
+
def test_child_iteration(self, doc):
|
|
47
|
+
books = list(doc.root)
|
|
48
|
+
assert [b.name for b in books] == ["book", "book"]
|
|
49
|
+
|
|
50
|
+
def test_attribute(self, doc):
|
|
51
|
+
book = doc.root.first_child_element
|
|
52
|
+
assert book.attribute("id") == "1"
|
|
53
|
+
assert book.attribute("lang") == "en"
|
|
54
|
+
|
|
55
|
+
def test_attribute_default(self, doc):
|
|
56
|
+
book = doc.root.first_child_element
|
|
57
|
+
assert book.attribute("nope") is None
|
|
58
|
+
assert book.attribute("nope", "x") == "x"
|
|
59
|
+
|
|
60
|
+
def test_attributes_iteration(self, doc):
|
|
61
|
+
book = doc.root.first_child_element
|
|
62
|
+
pairs = list(book.attributes())
|
|
63
|
+
assert pairs == [("id", "1"), ("lang", "en")]
|
|
64
|
+
assert book.attribute_count == 2
|
|
65
|
+
|
|
66
|
+
def test_attributes_entity_expansion_and_empty(self):
|
|
67
|
+
with Document.parse('<e t="a & b"/>') as doc:
|
|
68
|
+
assert list(doc.root.attributes()) == [("t", "a & b")]
|
|
69
|
+
with Document.parse("<e/>") as doc:
|
|
70
|
+
assert list(doc.root.attributes()) == []
|
|
71
|
+
assert doc.root.attribute_count == 0
|
|
72
|
+
|
|
73
|
+
def test_text(self, doc):
|
|
74
|
+
book = doc.root.first_child_element
|
|
75
|
+
assert book.text == "Ulysses"
|
|
76
|
+
|
|
77
|
+
def test_parent(self, doc):
|
|
78
|
+
book = doc.root.first_child_element
|
|
79
|
+
assert book.parent is not None
|
|
80
|
+
assert book.parent.name == "library"
|
|
81
|
+
|
|
82
|
+
def test_next_sibling(self, doc):
|
|
83
|
+
first = doc.root.first_child_element
|
|
84
|
+
second = first.next_sibling_element
|
|
85
|
+
assert second is not None
|
|
86
|
+
assert second.attribute("id") == "2"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestNode:
|
|
90
|
+
def test_node_types(self, doc):
|
|
91
|
+
node = doc.root.to_node()
|
|
92
|
+
types = set()
|
|
93
|
+
child = node.first_child
|
|
94
|
+
while child is not None:
|
|
95
|
+
types.add(child.type)
|
|
96
|
+
child = child.next_sibling
|
|
97
|
+
assert 1 in types # text
|
|
98
|
+
assert 2 in types # comment
|
|
99
|
+
|
|
100
|
+
def test_comment_content(self, doc):
|
|
101
|
+
node = doc.root.to_node().first_child
|
|
102
|
+
comments = []
|
|
103
|
+
while node is not None:
|
|
104
|
+
if node.is_comment():
|
|
105
|
+
comments.append(node.content)
|
|
106
|
+
node = node.next_sibling
|
|
107
|
+
# Comment content is the exact inner text, whitespace included.
|
|
108
|
+
assert comments == [" a comment "]
|
|
109
|
+
|
|
110
|
+
def test_child_count_counts_elements(self, doc):
|
|
111
|
+
# child_count is elements-only: root has 2 books plus
|
|
112
|
+
# interleaved text nodes that are not counted.
|
|
113
|
+
assert doc.root.to_node().child_count == 2
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestXPath:
|
|
117
|
+
def test_count(self, doc):
|
|
118
|
+
assert doc.xpath("count(//book)") == 2.0
|
|
119
|
+
|
|
120
|
+
def test_nodeset(self, doc):
|
|
121
|
+
books = doc.xpath("//book")
|
|
122
|
+
assert len(books) == 2
|
|
123
|
+
assert books[0].text == "Ulysses"
|
|
124
|
+
|
|
125
|
+
def test_string(self, doc):
|
|
126
|
+
assert doc.xpath("string(//book[@id='2'])") == "L'Etranger"
|
|
127
|
+
|
|
128
|
+
def test_boolean(self, doc):
|
|
129
|
+
assert doc.xpath("count(//book) = 2") is True
|
|
130
|
+
assert doc.xpath("count(//book) = 5") is False
|
|
131
|
+
|
|
132
|
+
def test_element_context(self, doc):
|
|
133
|
+
book = doc.root.first_child_element
|
|
134
|
+
assert book.xpath("string(@id)") == "1"
|
|
135
|
+
|
|
136
|
+
def test_error_raises(self, doc):
|
|
137
|
+
with pytest.raises(LeptrisError):
|
|
138
|
+
doc.xpath("///[")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TestSerialize:
|
|
142
|
+
def test_round_trip(self, doc):
|
|
143
|
+
out = doc.serialize()
|
|
144
|
+
assert "<library>" in out
|
|
145
|
+
reparsed = Document.parse(out)
|
|
146
|
+
assert reparsed.xpath("count(//book)") == 2.0
|
|
147
|
+
reparsed.close()
|