beresta 2.1.0__py3-none-any.whl
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.
- beresta/__init__.py +42 -0
- beresta/_diagnostics.py +45 -0
- beresta/_document_content.py +45 -0
- beresta/_element_attributes.py +49 -0
- beresta/_factory.py +53 -0
- beresta/_loader.py +110 -0
- beresta/_native/linux-aarch64/libberesta.so +0 -0
- beresta/_native/linux-x86_64/libberesta.so +0 -0
- beresta/_native/macos-aarch64/libberesta.dylib +0 -0
- beresta/_native/macos-x86_64/libberesta.dylib +0 -0
- beresta/_native/windows-aarch64/beresta.dll +0 -0
- beresta/_native/windows-x86_64/beresta.dll +0 -0
- beresta/_native.py +42 -0
- beresta/_node_children.py +44 -0
- beresta/_node_content.py +46 -0
- beresta/_node_editing.py +32 -0
- beresta/_node_navigation.py +32 -0
- beresta/_node_structure.py +50 -0
- beresta/_schema_streaming.py +57 -0
- beresta/_signatures.py +125 -0
- beresta/_wrapping.py +116 -0
- beresta/_xpath_scalars.py +52 -0
- beresta/document.py +49 -0
- beresta/element.py +29 -0
- beresta/errors.py +75 -0
- beresta/flags.py +50 -0
- beresta/languages.py +49 -0
- beresta/node.py +70 -0
- beresta/schema.py +64 -0
- beresta/xpath.py +71 -0
- beresta-2.1.0.dist-info/METADATA +74 -0
- beresta-2.1.0.dist-info/RECORD +35 -0
- beresta-2.1.0.dist-info/WHEEL +5 -0
- beresta-2.1.0.dist-info/licenses/LICENSE +21 -0
- beresta-2.1.0.dist-info/top_level.txt +1 -0
beresta/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Берёста — быстрая DOM-библиотека XML на C++17 для Python (через ctypes).
|
|
3
|
+
|
|
4
|
+
import beresta
|
|
5
|
+
|
|
6
|
+
with beresta.parse("<shop><item code='A1'>Чай</item></shop>") as document:
|
|
7
|
+
item = document.root.first_child
|
|
8
|
+
print(item.get_attribute("code"), item.text)
|
|
9
|
+
print(beresta.XPath("count(//item)").number(document))
|
|
10
|
+
|
|
11
|
+
Без зависимостей: нужен только сам Python и динамическая библиотека,
|
|
12
|
+
которую сборка кладёт в beresta/_native/<os>-<arch>/.
|
|
13
|
+
|
|
14
|
+
Beresta is a fast XML DOM library in C++17 for Python (through ctypes).
|
|
15
|
+
|
|
16
|
+
No dependencies: only Python itself and the native library,
|
|
17
|
+
which the build puts into beresta/_native/<os>-<arch>/.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from ._factory import new_document, parse, parse_file
|
|
21
|
+
from ._loader import platform_name
|
|
22
|
+
from ._native import LIB as _LIB
|
|
23
|
+
from .document import Document
|
|
24
|
+
from .element import Element
|
|
25
|
+
from .errors import BerestaError
|
|
26
|
+
from .flags import NodeType, ParseFlags, WriteFlags
|
|
27
|
+
from .languages import language, languages, set_language
|
|
28
|
+
from .node import Node
|
|
29
|
+
from .schema import Diagnostic, Schema
|
|
30
|
+
from .xpath import XPath
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"BerestaError", "Diagnostic", "Document", "Element", "Node", "NodeType",
|
|
34
|
+
"ParseFlags", "Schema", "WriteFlags", "XPath",
|
|
35
|
+
"language", "languages", "new_document", "parse", "parse_file", "platform_name",
|
|
36
|
+
"set_language", "version",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def version() -> str:
|
|
41
|
+
"""Версия нативной библиотеки, например "2.1.0". / The native library version, for example "2.1.0"."""
|
|
42
|
+
return _LIB.beresta_version().decode("ascii")
|
beresta/_diagnostics.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Нарушения схемы и чтение их нативного списка. Общее для проверки
|
|
3
|
+
готового дерева (Schema.validate) и потоковой проверки байтов и файлов
|
|
4
|
+
(Schema.validate_bytes / validate_file) — одна функция вместо трёх копий.
|
|
5
|
+
|
|
6
|
+
Schema violations and reading their native list. Common for the validation of
|
|
7
|
+
a ready tree (Schema.validate) and the streaming validation of bytes and files
|
|
8
|
+
(Schema.validate_bytes / validate_file) — one function instead of three copies.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import List
|
|
15
|
+
|
|
16
|
+
from ._native import LIB
|
|
17
|
+
from .errors import take_text
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Diagnostic:
|
|
22
|
+
"""Одно нарушение: текст с путём к элементу и место в исходнике (0 — неизвестно).
|
|
23
|
+
One violation: the text with the element path and the place in the source (0 — unknown)."""
|
|
24
|
+
|
|
25
|
+
message: str
|
|
26
|
+
line: int
|
|
27
|
+
column: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def take_diagnostics(diagnostics) -> List[Diagnostic]:
|
|
31
|
+
"""Список нарушений из нативного дескриптора; дескриптор освобождается в finally.
|
|
32
|
+
The list of violations from a native handle; the handle is freed in finally."""
|
|
33
|
+
try:
|
|
34
|
+
return [_diagnostic_at(diagnostics, index)
|
|
35
|
+
for index in range(LIB.beresta_diagnostics_count(diagnostics))]
|
|
36
|
+
finally:
|
|
37
|
+
LIB.beresta_diagnostics_free(diagnostics)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _diagnostic_at(diagnostics, index: int) -> Diagnostic:
|
|
41
|
+
"""Чтение одного нарушения из нативного списка. / Reads one violation from the native list."""
|
|
42
|
+
message = take_text(LIB.beresta_diagnostics_message(diagnostics, index))
|
|
43
|
+
line = LIB.beresta_diagnostics_line(diagnostics, index)
|
|
44
|
+
column = LIB.beresta_diagnostics_column(diagnostics, index)
|
|
45
|
+
return Diagnostic(message, line, column)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Примесь документа: корень, поиск по ID, копия, запись в файл.
|
|
2
|
+
Document mixin: the root, lookup by ID, a copy, writing to a file."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from ._native import LIB, encode, out_handle
|
|
10
|
+
from ._wrapping import wrap_node
|
|
11
|
+
from .errors import check
|
|
12
|
+
from .flags import WriteFlags
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DocumentContentMixin:
|
|
16
|
+
"""Операции уровня документа. / Document-level operations."""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def raw_handle(self) -> int:
|
|
20
|
+
"""Дескриптор без проверки закрытия (нужен фабрике обёрток).
|
|
21
|
+
The handle without the closed check (needed by the wrapper factory)."""
|
|
22
|
+
return self._pointer
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def root(self):
|
|
26
|
+
"""Корневой элемент или None. / The root element or None."""
|
|
27
|
+
return wrap_node(LIB.beresta_document_root(self.handle), self)
|
|
28
|
+
|
|
29
|
+
def element_by_id(self, identifier: str):
|
|
30
|
+
"""Элемент с атрибутом типа ID (по DTD) или xml:id.
|
|
31
|
+
An element with an ID-typed attribute (by DTD) or xml:id."""
|
|
32
|
+
return wrap_node(LIB.beresta_document_element_by_id(self.handle, encode(identifier)), self)
|
|
33
|
+
|
|
34
|
+
def clone(self):
|
|
35
|
+
"""Независимая глубокая копия документа. / An independent deep copy of the document."""
|
|
36
|
+
from .document import Document
|
|
37
|
+
|
|
38
|
+
result = out_handle()
|
|
39
|
+
check(LIB.beresta_document_clone(self.handle, result))
|
|
40
|
+
return Document(result.value)
|
|
41
|
+
|
|
42
|
+
def save(self, path, flags: WriteFlags = WriteFlags.DEFAULT, encoding: Optional[str] = None) -> None:
|
|
43
|
+
"""Запись в файл; encoding=None — UTF-8, иначе, например, "windows-1251".
|
|
44
|
+
Writing to a file; encoding=None means UTF-8, otherwise, for example, "windows-1251"."""
|
|
45
|
+
check(LIB.beresta_document_save(self.handle, encode(os.fspath(path)), int(flags), encode(encoding)))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Примесь: атрибуты элемента. / Mixin: element attributes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
|
|
7
|
+
from ._native import LIB, encode
|
|
8
|
+
from .errors import check, take_text
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ElementAttributesMixin:
|
|
12
|
+
"""Чтение, запись и перебор атрибутов (порядок — как в документе).
|
|
13
|
+
Reading, writing and iterating attributes (in document order)."""
|
|
14
|
+
|
|
15
|
+
def get_attribute(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
|
16
|
+
"""Значение атрибута или default, если атрибута нет.
|
|
17
|
+
The attribute value or default if there is no such attribute."""
|
|
18
|
+
value = take_text(LIB.beresta_element_get_attribute(self.handle, encode(name)))
|
|
19
|
+
if value is None:
|
|
20
|
+
return default
|
|
21
|
+
return value
|
|
22
|
+
|
|
23
|
+
def set_attribute(self, name: str, value: str) -> None:
|
|
24
|
+
check(LIB.beresta_element_set_attribute(self.handle, encode(name), encode(value)))
|
|
25
|
+
|
|
26
|
+
def has_attribute(self, name: str) -> bool:
|
|
27
|
+
result = LIB.beresta_element_has_attribute(self.handle, encode(name))
|
|
28
|
+
if result < 0:
|
|
29
|
+
check(LIB.beresta_last_error_code())
|
|
30
|
+
return result == 1
|
|
31
|
+
|
|
32
|
+
def remove_attribute(self, name: str) -> bool:
|
|
33
|
+
"""True — атрибут был и удалён, False — его не было.
|
|
34
|
+
True means the attribute existed and was removed, False means it did not exist."""
|
|
35
|
+
result = LIB.beresta_element_remove_attribute(self.handle, encode(name))
|
|
36
|
+
if result < 0:
|
|
37
|
+
check(LIB.beresta_last_error_code())
|
|
38
|
+
return result == 1
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def attributes(self) -> Dict[str, str]:
|
|
42
|
+
"""Снимок всех атрибутов: {имя: значение} в порядке документа.
|
|
43
|
+
A snapshot of all attributes: {name: value} in document order."""
|
|
44
|
+
result = {}
|
|
45
|
+
handle = self.handle
|
|
46
|
+
for index in range(LIB.beresta_element_attribute_count(handle)):
|
|
47
|
+
name = take_text(LIB.beresta_element_attribute_name(handle, index))
|
|
48
|
+
result[name] = take_text(LIB.beresta_element_attribute_value(handle, index))
|
|
49
|
+
return result
|
beresta/_factory.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Создание документов: разбор текста, байтов и файлов, документ с нуля.
|
|
2
|
+
Creating documents: parsing text, bytes and files, a document from scratch."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional, Union
|
|
8
|
+
|
|
9
|
+
from ._native import LIB, encode, out_handle
|
|
10
|
+
from .document import Document
|
|
11
|
+
from .errors import check
|
|
12
|
+
from .flags import ParseFlags
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _schema_handle(schema) -> Optional[int]:
|
|
16
|
+
"""Дескриптор схемы или NULL, если проверка по схеме не нужна.
|
|
17
|
+
The schema handle or NULL if no schema validation is needed."""
|
|
18
|
+
if schema is None:
|
|
19
|
+
return None
|
|
20
|
+
return schema.handle
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse(source: Union[str, bytes], flags: ParseFlags = ParseFlags.DEFAULT, schema=None) -> Document:
|
|
24
|
+
"""
|
|
25
|
+
Разбор XML. str кодируется в UTF-8; bytes передаются как есть —
|
|
26
|
+
кодировку (UTF-16, windows-1251, ...) библиотека определит сама.
|
|
27
|
+
|
|
28
|
+
Parses XML. str is encoded to UTF-8; bytes are passed as is —
|
|
29
|
+
the library detects the encoding (UTF-16, windows-1251, ...) itself.
|
|
30
|
+
"""
|
|
31
|
+
data = source
|
|
32
|
+
if isinstance(source, str):
|
|
33
|
+
data = source.encode("utf-8")
|
|
34
|
+
result = out_handle()
|
|
35
|
+
check(LIB.beresta_document_parse(data, len(data), int(flags), _schema_handle(schema), result))
|
|
36
|
+
return Document(result.value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_file(path, flags: ParseFlags = ParseFlags.DEFAULT, schema=None) -> Document:
|
|
40
|
+
"""Разбор файла (ParseFlags.FILE_RESOURCES разрешает внешние DTD рядом с ним).
|
|
41
|
+
Parses a file (ParseFlags.FILE_RESOURCES allows external DTDs next to it)."""
|
|
42
|
+
result = out_handle()
|
|
43
|
+
status = LIB.beresta_document_parse_file(encode(os.fspath(path)), int(flags), _schema_handle(schema), result)
|
|
44
|
+
check(status)
|
|
45
|
+
return Document(result.value)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def new_document(root_name: Optional[str] = None) -> Document:
|
|
49
|
+
"""Новый документ; root_name=None — без корневого элемента.
|
|
50
|
+
A new document; root_name=None means without a root element."""
|
|
51
|
+
result = out_handle()
|
|
52
|
+
check(LIB.beresta_document_create(encode(root_name), result))
|
|
53
|
+
return Document(result.value)
|
beresta/_loader.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Поиск и загрузка динамической библиотеки «Берёсты» через ctypes.
|
|
3
|
+
|
|
4
|
+
Порядок поиска (первый найденный файл побеждает):
|
|
5
|
+
1. переменная окружения BERESTA_LIBRARY — явный путь к файлу;
|
|
6
|
+
2. папка пакета _native/<os>-<arch>/ — туда бинарник кладут
|
|
7
|
+
CMake (-DBERESTA_BUILD_PYTHON=ON) и Makefile (make PYTHON=1);
|
|
8
|
+
3. системный поиск (ctypes.util.find_library) — библиотека,
|
|
9
|
+
установленная в систему (/usr/lib, PATH и т.п.).
|
|
10
|
+
|
|
11
|
+
Имена ОС и архитектуры совпадают с именами в CMake, Makefile и
|
|
12
|
+
Java-обёртке: windows/linux/macos и x86_64/aarch64/x86.
|
|
13
|
+
|
|
14
|
+
Finding and loading the Beresta shared library through ctypes.
|
|
15
|
+
Lookup order (the first file found wins):
|
|
16
|
+
1. the BERESTA_LIBRARY environment variable — an explicit file path;
|
|
17
|
+
2. the package folder _native/<os>-<arch>/ — the binary is put there by
|
|
18
|
+
CMake (-DBERESTA_BUILD_PYTHON=ON) and the Makefile (make PYTHON=1);
|
|
19
|
+
3. the system lookup (ctypes.util.find_library) — a library
|
|
20
|
+
installed in the system (/usr/lib, PATH and so on).
|
|
21
|
+
The OS and architecture names match CMake, the Makefile and the
|
|
22
|
+
Java wrapper: windows/linux/macos and x86_64/aarch64/x86.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import ctypes
|
|
28
|
+
import ctypes.util
|
|
29
|
+
import os
|
|
30
|
+
import platform
|
|
31
|
+
import struct
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
# Каноническое имя файла библиотеки на каждой ОС
|
|
36
|
+
# The canonical library file name on each OS
|
|
37
|
+
_LIBRARY_FILES = {
|
|
38
|
+
"windows": "beresta.dll",
|
|
39
|
+
"linux": "libberesta.so",
|
|
40
|
+
"macos": "libberesta.dylib",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Разные системы называют одну архитектуру по-разному
|
|
44
|
+
# Different systems name the same architecture differently
|
|
45
|
+
_ARCH_ALIASES = {
|
|
46
|
+
"amd64": "x86_64", "x86_64": "x86_64", "x64": "x86_64",
|
|
47
|
+
"arm64": "aarch64", "aarch64": "aarch64",
|
|
48
|
+
"i386": "x86", "i686": "x86", "x86": "x86",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def platform_name() -> str:
|
|
53
|
+
"""Имя платформы вида windows-x86_64 (имя папки в _native).
|
|
54
|
+
The platform name like windows-x86_64 (the folder name in _native)."""
|
|
55
|
+
if sys.platform.startswith("win"):
|
|
56
|
+
system = "windows"
|
|
57
|
+
elif sys.platform == "darwin":
|
|
58
|
+
system = "macos"
|
|
59
|
+
else:
|
|
60
|
+
system = sys.platform.rstrip("0123456789")
|
|
61
|
+
machine = platform.machine().lower()
|
|
62
|
+
arch = _ARCH_ALIASES.get(machine, machine)
|
|
63
|
+
# 32-битный Python на 64-битном процессоре загрузит только 32-битную библиотеку
|
|
64
|
+
# A 32-bit Python on a 64-bit processor can load only a 32-bit library
|
|
65
|
+
if arch == "x86_64" and struct.calcsize("P") == 4:
|
|
66
|
+
arch = "x86"
|
|
67
|
+
return system + "-" + arch
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def library_file_name() -> str:
|
|
71
|
+
"""Имя файла библиотеки для текущей ОС. / The library file name for the current OS."""
|
|
72
|
+
system = platform_name().split("-")[0]
|
|
73
|
+
return _LIBRARY_FILES.get(system, "libberesta.so")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _candidates() -> list:
|
|
77
|
+
"""Кандидаты в порядке приоритета (несуществующие отбрасываются позже).
|
|
78
|
+
Candidates in priority order (missing ones are dropped later)."""
|
|
79
|
+
result = []
|
|
80
|
+
explicit = os.environ.get("BERESTA_LIBRARY")
|
|
81
|
+
if explicit:
|
|
82
|
+
result.append(explicit)
|
|
83
|
+
bundled = Path(__file__).resolve().parent / "_native" / platform_name() / library_file_name()
|
|
84
|
+
result.append(str(bundled))
|
|
85
|
+
system_wide = ctypes.util.find_library("beresta")
|
|
86
|
+
if system_wide:
|
|
87
|
+
result.append(system_wide)
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_library() -> ctypes.CDLL:
|
|
92
|
+
"""Загрузка библиотеки; понятная ошибка, если бинарник не найден.
|
|
93
|
+
Loads the library; a clear error if the binary is not found."""
|
|
94
|
+
tried = []
|
|
95
|
+
for candidate in _candidates():
|
|
96
|
+
if os.path.isabs(candidate) and not os.path.exists(candidate):
|
|
97
|
+
tried.append(candidate)
|
|
98
|
+
continue
|
|
99
|
+
return ctypes.CDLL(candidate)
|
|
100
|
+
# Библиотека ещё не загружена, каталога сообщений нет — текст на двух языках
|
|
101
|
+
# The library is not loaded yet, so there is no message catalog — the text is bilingual
|
|
102
|
+
raise OSError(
|
|
103
|
+
"Берёста: динамическая библиотека не найдена для платформы "
|
|
104
|
+
+ platform_name() + ". Соберите её с флагом PYTHON=1 (make) или "
|
|
105
|
+
+ "-DBERESTA_BUILD_PYTHON=ON (CMake) либо задайте BERESTA_LIBRARY. "
|
|
106
|
+
+ "Проверенные пути: " + ", ".join(tried)
|
|
107
|
+
+ " / Beresta: the native library was not found for the platform "
|
|
108
|
+
+ platform_name() + ". Build it with PYTHON=1 (make) or "
|
|
109
|
+
+ "-DBERESTA_BUILD_PYTHON=ON (CMake), or set BERESTA_LIBRARY. "
|
|
110
|
+
+ "Paths tried: " + ", ".join(tried))
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
beresta/_native.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Единственный экземпляр загруженной библиотеки (паттерн Singleton на уровне
|
|
3
|
+
модуля: Python импортирует модуль ровно один раз, поэтому библиотека
|
|
4
|
+
загружается и связывается с сигнатурами тоже один раз).
|
|
5
|
+
|
|
6
|
+
The single instance of the loaded library (the Singleton pattern at module
|
|
7
|
+
level: Python imports a module exactly once, so the library is also
|
|
8
|
+
loaded and bound to the signatures once).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from ctypes import c_void_p
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from ._loader import load_library
|
|
17
|
+
from ._signatures import bind_signatures
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _initialize():
|
|
21
|
+
"""Загрузка библиотеки и назначение сигнатур всем функциям.
|
|
22
|
+
Loads the library and assigns signatures to all functions."""
|
|
23
|
+
library = load_library()
|
|
24
|
+
bind_signatures(library)
|
|
25
|
+
return library
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
LIB = _initialize()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def encode(text: Optional[str]) -> Optional[bytes]:
|
|
32
|
+
"""Строка Python -> байты UTF-8 для параметра const char* (None -> NULL).
|
|
33
|
+
A Python string -> UTF-8 bytes for a const char* parameter (None -> NULL)."""
|
|
34
|
+
if text is None:
|
|
35
|
+
return None
|
|
36
|
+
return str(text).encode("utf-8")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def out_handle() -> c_void_p:
|
|
40
|
+
"""Пустая ячейка для выходного параметра вида beresta_document* out.
|
|
41
|
+
An empty cell for an output parameter like beresta_document* out."""
|
|
42
|
+
return c_void_p()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Примесь: доступ к детям как к последовательности Python. / Mixin: access to children as a Python sequence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterator
|
|
6
|
+
|
|
7
|
+
from ._native import LIB
|
|
8
|
+
from ._wrapping import wrap_node
|
|
9
|
+
from .errors import check
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NodeChildrenMixin:
|
|
13
|
+
"""len(node), node[i], for child in node: ..."""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def child_count(self) -> int:
|
|
17
|
+
count = LIB.beresta_node_child_count(self.handle)
|
|
18
|
+
if count < 0:
|
|
19
|
+
check(LIB.beresta_last_error_code())
|
|
20
|
+
return count
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def children(self) -> list:
|
|
24
|
+
"""Список детей (снимок: последующие изменения дерева в нём не видны).
|
|
25
|
+
The list of children (a snapshot: later tree changes are not visible in it)."""
|
|
26
|
+
return list(iter(self))
|
|
27
|
+
|
|
28
|
+
def __len__(self) -> int:
|
|
29
|
+
return self.child_count
|
|
30
|
+
|
|
31
|
+
def __getitem__(self, index: int):
|
|
32
|
+
"""Ребёнок по индексу; отрицательный индекс — с конца, как у list.
|
|
33
|
+
A child by index; a negative index counts from the end, as in list."""
|
|
34
|
+
if index < 0:
|
|
35
|
+
index += self.child_count
|
|
36
|
+
return wrap_node(LIB.beresta_node_child(self.handle, index), self.document)
|
|
37
|
+
|
|
38
|
+
def __iter__(self) -> Iterator:
|
|
39
|
+
"""Обход детей через соседей — O(1) на шаг, без лишних списков.
|
|
40
|
+
Iterates children through siblings — O(1) per step, without extra lists."""
|
|
41
|
+
child = self.first_child
|
|
42
|
+
while child is not None:
|
|
43
|
+
yield child
|
|
44
|
+
child = child.next_sibling
|
beresta/_node_content.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Примесь: тип, имя, текст и сериализация узла. / Mixin: node type, name, text and serialization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._native import LIB, encode
|
|
6
|
+
from .errors import check, take_text
|
|
7
|
+
from .flags import NodeType, WriteFlags
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NodeContentMixin:
|
|
11
|
+
"""Свойства содержимого узла. / Node content properties."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def type(self) -> NodeType:
|
|
15
|
+
"""Тип узла (NodeType); запоминается при первом запросе или фабрикой.
|
|
16
|
+
The node type (NodeType); remembered on the first request or by the factory."""
|
|
17
|
+
handle = self.handle
|
|
18
|
+
if self._kind is None:
|
|
19
|
+
code = LIB.beresta_node_type(handle)
|
|
20
|
+
if code < 0:
|
|
21
|
+
check(LIB.beresta_last_error_code())
|
|
22
|
+
self._kind = code
|
|
23
|
+
return NodeType(self._kind)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def name(self) -> str:
|
|
27
|
+
"""Имя по соглашению DOM: тег, цель PI, "#text", "#comment"...
|
|
28
|
+
The name by the DOM convention: a tag, a PI target, "#text", "#comment"..."""
|
|
29
|
+
return take_text(LIB.beresta_node_name(self.handle))
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def text(self) -> str:
|
|
33
|
+
"""Текстовое содержимое (у элемента — весь текст потомков).
|
|
34
|
+
The text content (for an element — all text of its descendants)."""
|
|
35
|
+
return take_text(LIB.beresta_node_text(self.handle))
|
|
36
|
+
|
|
37
|
+
@text.setter
|
|
38
|
+
def text(self, value: str) -> None:
|
|
39
|
+
"""У элемента все дети заменяются одним текстовым узлом.
|
|
40
|
+
For an element all children are replaced with one text node."""
|
|
41
|
+
check(LIB.beresta_node_set_text(self.handle, encode(value)))
|
|
42
|
+
|
|
43
|
+
def to_xml(self, flags: WriteFlags = WriteFlags.DEFAULT) -> str:
|
|
44
|
+
"""Сериализация узла с потомками (по умолчанию — без лишних пробелов).
|
|
45
|
+
Serializes the node with its descendants (by default — without extra spaces)."""
|
|
46
|
+
return take_text(LIB.beresta_node_to_string(self.handle, int(flags)))
|
beresta/_node_editing.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Примесь: добавление новых узлов в конец детей. / Mixin: appending new nodes to the end of the children."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ._native import LIB, encode
|
|
8
|
+
from ._wrapping import wrap_node
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NodeEditingMixin:
|
|
12
|
+
"""Каждый метод возвращает созданный узел — удобно строить дерево цепочкой.
|
|
13
|
+
Each method returns the created node — convenient for building a tree in a chain."""
|
|
14
|
+
|
|
15
|
+
def append_element(self, name: str):
|
|
16
|
+
"""Новый элемент; у пустого документа он становится корнем.
|
|
17
|
+
A new element; in an empty document it becomes the root."""
|
|
18
|
+
return wrap_node(LIB.beresta_node_append_element(self.handle, encode(name)), self.document)
|
|
19
|
+
|
|
20
|
+
def append_text(self, text: str):
|
|
21
|
+
return wrap_node(LIB.beresta_node_append_text(self.handle, encode(text)), self.document)
|
|
22
|
+
|
|
23
|
+
def append_comment(self, text: str):
|
|
24
|
+
return wrap_node(LIB.beresta_node_append_comment(self.handle, encode(text)), self.document)
|
|
25
|
+
|
|
26
|
+
def append_cdata(self, text: str):
|
|
27
|
+
return wrap_node(LIB.beresta_node_append_cdata(self.handle, encode(text)), self.document)
|
|
28
|
+
|
|
29
|
+
def append_pi(self, target: str, data: Optional[str] = None):
|
|
30
|
+
"""Инструкция обработки <?target data?>. / A processing instruction <?target data?>."""
|
|
31
|
+
pointer = LIB.beresta_node_append_pi(self.handle, encode(target), encode(data))
|
|
32
|
+
return wrap_node(pointer, self.document)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Примесь: навигация по дереву (родитель, соседи, первый и последний ребёнок).
|
|
2
|
+
Mixin: tree navigation (parent, siblings, first and last child)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from ._native import LIB
|
|
7
|
+
from ._wrapping import wrap_node
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NodeNavigationMixin:
|
|
11
|
+
"""Соседи и родитель находятся за O(1): ядро хранит индекс узла у родителя.
|
|
12
|
+
Siblings and the parent are found in O(1): the core stores the node's index in its parent."""
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def parent(self):
|
|
16
|
+
return wrap_node(LIB.beresta_node_parent(self.handle), self.document)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def first_child(self):
|
|
20
|
+
return wrap_node(LIB.beresta_node_first_child(self.handle), self.document)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def last_child(self):
|
|
24
|
+
return wrap_node(LIB.beresta_node_last_child(self.handle), self.document)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def next_sibling(self):
|
|
28
|
+
return wrap_node(LIB.beresta_node_next_sibling(self.handle), self.document)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def previous_sibling(self):
|
|
32
|
+
return wrap_node(LIB.beresta_node_previous_sibling(self.handle), self.document)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Примесь: удаление, копирование и перенос поддерева. / Mixin: removing, copying and moving a subtree."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._native import LIB, out_handle
|
|
6
|
+
from ._wrapping import transfer_wrappers, wrap_node
|
|
7
|
+
from .errors import check
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class NodeStructureMixin:
|
|
11
|
+
"""Изменение структуры дерева. / Changing the tree structure."""
|
|
12
|
+
|
|
13
|
+
def remove(self) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Удаление узла с потомками. После вызова этот объект и обёртки
|
|
16
|
+
потомков недействительны — обращаться к ним нельзя.
|
|
17
|
+
|
|
18
|
+
Removes the node with its descendants. After the call this object and the
|
|
19
|
+
wrappers of its descendants are invalid — they must not be used.
|
|
20
|
+
"""
|
|
21
|
+
check(LIB.beresta_node_remove(self.handle))
|
|
22
|
+
|
|
23
|
+
def copy_to(self, parent):
|
|
24
|
+
"""Глубокая копия в конец детей parent (можно в другой документ).
|
|
25
|
+
A deep copy appended to the children of parent (another document is allowed)."""
|
|
26
|
+
result = out_handle()
|
|
27
|
+
check(LIB.beresta_node_copy_to(self.handle, parent.handle, result))
|
|
28
|
+
return wrap_node(result.value, parent.document)
|
|
29
|
+
|
|
30
|
+
def move_to(self, parent) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Перенос (без копирования) в конец детей parent, в том числе в другой
|
|
33
|
+
документ. Этот объект остаётся тем же узлом; после переноса его
|
|
34
|
+
документ — parent.document, и закрытие прежнего документа узел не
|
|
35
|
+
затрагивает. Обёртки потомков, полученные раньше, тоже переходят
|
|
36
|
+
к новому документу. Корень документа и сам документ переносить
|
|
37
|
+
нельзя (BerestaError HIERARCHY_VIOLATION / INVALID_ARGUMENT).
|
|
38
|
+
|
|
39
|
+
Moves the node (without copying) to the end of the children of parent,
|
|
40
|
+
including into another document. This object stays the same node; after the
|
|
41
|
+
move its document is parent.document, and closing the former document does
|
|
42
|
+
not affect the node. The wrappers of descendants obtained earlier move to the
|
|
43
|
+
new document too. The document root and the document itself cannot be moved
|
|
44
|
+
(BerestaError HIERARCHY_VIOLATION / INVALID_ARGUMENT).
|
|
45
|
+
"""
|
|
46
|
+
source = self.document
|
|
47
|
+
target = parent.document
|
|
48
|
+
check(LIB.beresta_node_move_to(self.handle, parent.handle))
|
|
49
|
+
if source is not target:
|
|
50
|
+
transfer_wrappers(self, source, target)
|