diff --git a/python/markupever/_rustlib.pyi b/python/markupever/_rustlib.pyi deleted file mode 100644 index e40ee0c..0000000 --- a/python/markupever/_rustlib.pyi +++ /dev/null @@ -1,179 +0,0 @@ -import typing - -__version__: str -__author__: str - -QUIRKS_MODE_FULL: typing.Literal[0] -QUIRKS_MODE_LIMITED: typing.Literal[1] -QUIRKS_MODE_OFF: typing.Literal[2] - -class HtmlOptions: - """ - These are options for HTML parsing. - - Note: this type is immutable. - """ - - def __new__( - cls: type, - full_document=..., - exact_errors=..., - discard_bom=..., - profile=..., - iframe_srcdoc=..., - drop_doctype=..., - quirks_mode=..., - fragment_context: QualName | str | None = ..., - ) -> typing.Self: - """ - Creates a new `HtmlOptions` - - - `full_document`: Is this a complete document? (means includes html, head, and body tag). Default: true, - or false if `fragment_context` is given. - - `exact_errors`: Report all parse errors described in the spec, at some performance penalty? Default: false. - - `discard_bom`: Discard a `U+FEFF BYTE ORDER MARK` if we see one at the beginning of the stream? Default: true. - - `profile`: Keep a record of how long we spent in each state? Printed when `finish()` is called. Default: false. - - `iframe_srcdoc`: Is this an `iframe srcdoc` document? Default: false. - - `drop_doctype`: Should we drop the DOCTYPE (if any) from the tree? Default: false. - - `quirks_mode`: Initial TreeBuilder quirks mode. Default: QUIRKS_MODE_OFF. - - `fragment_context`: Parse a fragment as if it were the contents of this context element, e.g. `"td"` or - `QualName("path", "svg")`. A name without a namespace is an HTML element. Default: None (fragments are - parsed in a `body` context). - """ - - @property - def full_document(self) -> bool: ... - @property - def fragment_context(self) -> QualName | None: ... - @property - def exact_errors(self) -> bool: ... - @property - def discard_bom(self) -> bool: ... - @property - def profile(self) -> bool: ... - @property - def iframe_srcdoc(self) -> bool: ... - @property - def drop_doctype(self) -> bool: ... - @property - def quirks_mode(self) -> int: ... - -class XmlOptions: - """ - These are options for XML parsing. - - Note: this type is immutable. - """ - - def __new__( - cls: type, - exact_errors=..., - discard_bom=..., - profile=..., - ) -> typing.Self: - """ - Creates a new `XmlOptions` - - - `exact_errors`: Report all parse errors described in the spec, at some performance penalty? Default: false. - - `discard_bom`: Discard a `U+FEFF BYTE ORDER MARK` if we see one at the beginning of the stream? Default: true. - - `profile`: Keep a record of how long we spent in each state? Printed when `finish()` is called. Default: false. - """ - - @property - def exact_errors(self) -> bool: ... - @property - def discard_bom(self) -> bool: ... - @property - def profile(self) -> bool: ... - -class QualName: - """ - A fully qualified name (with a namespace), used to depict names of tags and attributes. - - Namespaces can be used to differentiate between similar XML fragments. For example: - - ``` - // HTML - - - - - -
ApplesBananas
- - // Furniture XML - - African Coffee Table - 80 - 120 -
- ``` - - Without XML namespaces, we can't use those two fragments in the same document - at the same time. However if we declare a namespace we could instead say: - - ``` - // Furniture XML - - African Coffee Table - 80 - 120 - - ``` - - and bind the prefix `furn` to a different namespace. - - For this reason we parse names that contain a colon in the following way: - - ``` - - | | - | +- local name - | - prefix (when resolved gives namespace_url `https://furniture.rs`) - ``` - - Note: This type is immutable. - """ - - def __new__( - cls, - local: str, - ns: str - | typing.Literal[ - "html", "xml", "xhtml", "xmlns", "xlink", "svg", "mathml", "*" - ] = ..., - prefix: str | None = ..., - ): ... - @property - def local(self) -> str: - """The local name (e.g. `table` in `` above).""" - @property - def ns(self) -> str: - """The namespace after resolution (e.g. https://furniture.rs in example above).""" - @property - def prefix(self) -> str | None: - """ - The prefix of qualified (e.g. furn in above). - Optional (since some namespaces can be empty or inferred), - and only useful for namespace resolution (since different prefix can still resolve to same namespace) - """ - - def copy(self) -> QualName: - """ - Create a copy of the current QualName instance. - - Returns a new QualName instance with the same local name, namespace, and prefix. - """ - - def __eq__(self, value) -> bool: ... - def __ne__(self, value) -> bool: ... - def __gt__(self, value) -> bool: ... - def __ge__(self, value) -> bool: ... - def __lt__(self, value) -> bool: ... - def __le__(self, value) -> bool: ... - def __hash__(self) -> int: ... - -class AttrsListItems: - def __iter__(self) -> AttrsListItems: ... - def __next__(self) -> tuple[QualName, str]: ... diff --git a/python/markupever/_rustlib/__init__.pyi b/python/markupever/_rustlib/__init__.pyi new file mode 100644 index 0000000..26a8dbe --- /dev/null +++ b/python/markupever/_rustlib/__init__.pyi @@ -0,0 +1,352 @@ +import typing + +from . import iter as iter + +__all__ = [ + "QUIRKS_MODE_FULL", + "QUIRKS_MODE_LIMITED", + "QUIRKS_MODE_OFF", + "AttrsList", + "AttrsListItems", + "Comment", + "Doctype", + "Document", + "Element", + "HtmlOptions", + "Parser", + "ProcessingInstruction", + "QualName", + "Select", + "Text", + "TreeDom", + "XmlOptions", + "__author__", + "__version__", + "_is_node_impl", + "iter", + "serialize", +] + +__version__: str +__author__: str + +QUIRKS_MODE_FULL: typing.Literal[0] +QUIRKS_MODE_LIMITED: typing.Literal[1] +QUIRKS_MODE_OFF: typing.Literal[2] + +@typing.final +class HtmlOptions: + """ + These are options for HTML parsing. + + Note: this type is immutable. + """ + + def __new__( + cls, + full_document: bool | None = ..., + exact_errors: bool = ..., + discard_bom: bool = ..., + profile: bool = ..., + iframe_srcdoc: bool = ..., + drop_doctype: bool = ..., + quirks_mode: int = ..., + fragment_context: QualName | str | None = ..., + ) -> HtmlOptions: + """ + Creates a new `HtmlOptions` + + - `full_document`: Is this a complete document? (means includes html, head, and body tag). Default: true, + or false if `fragment_context` is given. + - `exact_errors`: Report all parse errors described in the spec, at some performance penalty? Default: false. + - `discard_bom`: Discard a `U+FEFF BYTE ORDER MARK` if we see one at the beginning of the stream? Default: true. + - `profile`: Keep a record of how long we spent in each state? Printed when `finish()` is called. Default: false. + - `iframe_srcdoc`: Is this an `iframe srcdoc` document? Default: false. + - `drop_doctype`: Should we drop the DOCTYPE (if any) from the tree? Default: false. + - `quirks_mode`: Initial TreeBuilder quirks mode. Default: QUIRKS_MODE_OFF. + - `fragment_context`: Parse a fragment as if it were the contents of this context element, e.g. `"td"` or + `QualName("path", "svg")`. A name without a namespace is an HTML element. Default: None (fragments are + parsed in a `body` context). + """ + + @property + def full_document(self) -> bool: ... + @property + def fragment_context(self) -> QualName | None: ... + @property + def exact_errors(self) -> bool: ... + @property + def discard_bom(self) -> bool: ... + @property + def profile(self) -> bool: ... + @property + def iframe_srcdoc(self) -> bool: ... + @property + def drop_doctype(self) -> bool: ... + @property + def quirks_mode(self) -> int: ... + +@typing.final +class XmlOptions: + """ + These are options for XML parsing. + + Note: this type is immutable. + """ + + def __new__( + cls, + exact_errors: bool = ..., + discard_bom: bool = ..., + profile: bool = ..., + ) -> XmlOptions: + """ + Creates a new `XmlOptions` + + - `exact_errors`: Report all parse errors described in the spec, at some performance penalty? Default: false. + - `discard_bom`: Discard a `U+FEFF BYTE ORDER MARK` if we see one at the beginning of the stream? Default: true. + - `profile`: Keep a record of how long we spent in each state? Printed when `finish()` is called. Default: false. + """ + + @property + def exact_errors(self) -> bool: ... + @property + def discard_bom(self) -> bool: ... + @property + def profile(self) -> bool: ... + +@typing.final +class QualName: + """ + A fully qualified name (with a namespace), used to depict names of tags and attributes. + + Namespaces can be used to differentiate between similar XML fragments. For example: + + ``` + // HTML + + + + + +
ApplesBananas
+ + // Furniture XML + + African Coffee Table + 80 + 120 +
+ ``` + + Without XML namespaces, we can't use those two fragments in the same document + at the same time. However if we declare a namespace we could instead say: + + ``` + // Furniture XML + + African Coffee Table + 80 + 120 + + ``` + + and bind the prefix `furn` to a different namespace. + + For this reason we parse names that contain a colon in the following way: + + ``` + + | | + | +- local name + | + prefix (when resolved gives namespace_url `https://furniture.rs`) + ``` + + Note: This type is immutable. + """ + + def __new__( + cls, + local: str, + ns: str + | typing.Literal[ + "html", "xml", "xhtml", "xmlns", "xlink", "svg", "mathml", "*" + ] = ..., + prefix: str | None = ..., + ): ... + @property + def local(self) -> str: + """The local name (e.g. `table` in `` above).""" + @property + def ns(self) -> str: + """The namespace after resolution (e.g. https://furniture.rs in example above).""" + @property + def prefix(self) -> str | None: + """ + The prefix of qualified (e.g. furn in above). + Optional (since some namespaces can be empty or inferred), + and only useful for namespace resolution (since different prefix can still resolve to same namespace) + """ + + def copy(self) -> QualName: + """ + Create a copy of the current QualName instance. + + Returns a new QualName instance with the same local name, namespace, and prefix. + """ + + def __eq__(self, value: object, /) -> bool: ... + def __ne__(self, value: object, /) -> bool: ... + def __gt__(self, value: QualName, /) -> bool: ... + def __ge__(self, value: QualName, /) -> bool: ... + def __lt__(self, value: QualName, /) -> bool: ... + def __le__(self, value: QualName, /) -> bool: ... + def __hash__(self) -> int: ... + +_QualNameOrStr: typing.TypeAlias = QualName | str +_Node: typing.TypeAlias = ( + Document | Doctype | Comment | Text | Element | ProcessingInstruction +) + +@typing.final +class TreeDom: + """A tree of nodes, which owns all of its nodes.""" + + def __new__(cls, *, namespaces: dict[str, str] | None = ...) -> TreeDom: ... + @classmethod + def with_capacity( + cls, capacity: int, *, namespaces: dict[str, str] | None = ... + ) -> TreeDom: ... + def namespaces(self) -> dict[str, str]: ... + def root(self) -> Document: ... + def append(self, parent: _Node, child: _Node) -> None: ... + def prepend(self, parent: _Node, child: _Node) -> None: ... + def insert_before(self, sibling: _Node, new_sibling: _Node) -> None: ... + def insert_after(self, sibling: _Node, new_sibling: _Node) -> None: ... + def detach(self, node: _Node) -> None: ... + def reparent_append(self, parent: _Node, child: _Node) -> None: ... + def reparent_prepend(self, parent: _Node, child: _Node) -> None: ... + def __eq__(self, value: object, /) -> bool: ... + def __ne__(self, value: object, /) -> bool: ... + __hash__: typing.ClassVar[None] # type: ignore[assignment] + def __len__(self) -> int: ... + +class _NodeMethods: + """Methods that all node types have (not a real class).""" + + def tree(self) -> TreeDom: ... + def parent(self) -> _Node | None: ... + def prev_sibling(self) -> _Node | None: ... + def next_sibling(self) -> _Node | None: ... + def first_child(self) -> _Node | None: ... + def last_child(self) -> _Node | None: ... + def has_children(self) -> bool: ... + def has_siblings(self) -> bool: ... + def __eq__(self, value: object, /) -> bool: ... + def __ne__(self, value: object, /) -> bool: ... + __hash__: typing.ClassVar[None] # type: ignore[assignment] + +@typing.final +class Document(_NodeMethods): + """A document node, the root of a tree. It has no constructor.""" + + def __new__(cls, treedom: object) -> typing.NoReturn: ... + +@typing.final +class Doctype(_NodeMethods): + def __new__( + cls, treedom: TreeDom, name: str, public_id: str, system_id: str + ) -> Doctype: ... + name: str + public_id: str + system_id: str + +@typing.final +class Comment(_NodeMethods): + def __new__(cls, treedom: TreeDom, content: str) -> Comment: ... + content: str + +@typing.final +class Text(_NodeMethods): + def __new__(cls, treedom: TreeDom, content: str) -> Text: ... + content: str + +@typing.final +class Element(_NodeMethods): + def __new__( + cls, + treedom: TreeDom, + name: _QualNameOrStr, + attrs: typing.Iterable[tuple[_QualNameOrStr, str]], + template: bool, + mathml_annotation_xml_integration_point: bool, + ) -> Element: ... + @property + def name(self) -> QualName: ... + @name.setter + def name(self, value: _QualNameOrStr) -> None: ... + @property + def attrs(self) -> AttrsList: ... + @attrs.setter + def attrs(self, value: typing.Iterable[tuple[_QualNameOrStr, str]]) -> None: ... + template: bool + mathml_annotation_xml_integration_point: bool + def id(self) -> str | None: ... + def class_list(self) -> list[str]: ... + +@typing.final +class ProcessingInstruction(_NodeMethods): + def __new__( + cls, treedom: TreeDom, data: str, target: str + ) -> ProcessingInstruction: ... + data: str + target: str + +@typing.final +class AttrsList: + """The attributes of an element. It has no constructor.""" + + def __new__(cls, *args: object, **kwargs: object) -> typing.NoReturn: ... + def clear(self) -> None: ... + def insert(self, index: int, key: _QualNameOrStr, value: str) -> None: ... + def push(self, key: _QualNameOrStr, value: str) -> None: ... + def items(self) -> AttrsListItems: ... + def update_item(self, index: int, key: _QualNameOrStr, value: str) -> None: ... + def update_value(self, index: int, value: str) -> None: ... + def get_by_index(self, index: int) -> tuple[QualName, str]: ... + def remove(self, index: int) -> tuple[QualName, str]: ... + def swap_remove(self, index: int) -> tuple[QualName, str]: ... + def dedup(self) -> None: ... + def reverse(self) -> None: ... + def __len__(self) -> int: ... + +@typing.final +class AttrsListItems: + def __new__(cls, *args: object, **kwargs: object) -> typing.NoReturn: ... + def __iter__(self) -> AttrsListItems: ... + def __next__(self) -> tuple[QualName, str]: ... + def __len__(self) -> int: ... + +@typing.final +class Parser: + def __new__(cls, options: HtmlOptions | XmlOptions) -> Parser: ... + def process(self, content: str | bytes) -> None: ... + def finish(self) -> None: ... + def into_dom(self) -> TreeDom: ... + def errors(self) -> list[str]: ... + def quirks_mode(self) -> int: ... + def lineno(self) -> int: ... + +@typing.final +class Select: + """An iterator over the elements matching a CSS selector, in document order.""" + + def __new__(cls, node: _Node, expression: str) -> Select: ... + def __iter__(self) -> Select: ... + def __next__(self) -> Element: ... + +def serialize( + node: _Node, indent: int = ..., include_self: bool = ..., is_html: bool | None = ... +) -> bytes: ... +def _is_node_impl(object: object) -> bool: ... diff --git a/python/markupever/_rustlib/iter.pyi b/python/markupever/_rustlib/iter.pyi new file mode 100644 index 0000000..12e6cfc --- /dev/null +++ b/python/markupever/_rustlib/iter.pyi @@ -0,0 +1,75 @@ +"""Iterators over nodes.""" + +import typing + +from . import TreeDom, _Node + +__all__ = [ + "Ancestors", + "Children", + "Descendants", + "FirstChildren", + "Iterator", + "LastChildren", + "NextSiblings", + "PrevSiblings", + "Traverse", +] + +@typing.final +class Iterator: + """Iterates over all of the nodes of a tree in the order they were created, including orphans.""" + + def __new__(cls, dom: TreeDom) -> Iterator: ... + def __iter__(self) -> Iterator: ... + def __next__(self) -> _Node: ... + +@typing.final +class Ancestors: + def __new__(cls, node: _Node) -> Ancestors: ... + def __iter__(self) -> Ancestors: ... + def __next__(self) -> _Node: ... + +@typing.final +class PrevSiblings: + def __new__(cls, node: _Node) -> PrevSiblings: ... + def __iter__(self) -> PrevSiblings: ... + def __next__(self) -> _Node: ... + +@typing.final +class NextSiblings: + def __new__(cls, node: _Node) -> NextSiblings: ... + def __iter__(self) -> NextSiblings: ... + def __next__(self) -> _Node: ... + +@typing.final +class FirstChildren: + def __new__(cls, node: _Node) -> FirstChildren: ... + def __iter__(self) -> FirstChildren: ... + def __next__(self) -> _Node: ... + +@typing.final +class LastChildren: + def __new__(cls, node: _Node) -> LastChildren: ... + def __iter__(self) -> LastChildren: ... + def __next__(self) -> _Node: ... + +@typing.final +class Children: + def __new__(cls, node: _Node) -> Children: ... + def __iter__(self) -> Children: ... + def __next__(self) -> _Node: ... + +@typing.final +class Traverse: + """Iterates over the edges of a subtree: each node with `False` when opened and `True` when closed.""" + + def __new__(cls, node: _Node) -> Traverse: ... + def __iter__(self) -> Traverse: ... + def __next__(self) -> tuple[_Node, bool]: ... + +@typing.final +class Descendants: + def __new__(cls, node: _Node) -> Descendants: ... + def __iter__(self) -> Descendants: ... + def __next__(self) -> _Node: ... diff --git a/python/tests/test_rustlib.py b/python/tests/test_rustlib.py index 6545e6e..32824f7 100644 --- a/python/tests/test_rustlib.py +++ b/python/tests/test_rustlib.py @@ -624,3 +624,13 @@ def test_serialize(): assert rl.serialize(dom.root(), 0) == b"Ali" assert rl.serialize(dom.root(), 0, is_html=False) == b"Ali" + + +def test_iter_submodule(): + import markupever._rustlib.iter + + assert markupever._rustlib.iter is rl.iter + assert rl.iter.__name__ == "markupever._rustlib.iter" + + for name in rl.iter.__all__: + assert getattr(rl.iter, name).__module__ == "markupever._rustlib.iter", name diff --git a/src/iter/iterator.rs b/src/iter/iterator.rs index ca89da2..aebaea8 100644 --- a/src/iter/iterator.rs +++ b/src/iter/iterator.rs @@ -2,7 +2,7 @@ use std::sync::atomic; use std::sync::Arc; /// Iterates the nodes in insert order - don't matter which are orphan which not -#[pyo3::pyclass(name = "Iterator", module = "markupever._rustlib", frozen)] +#[pyo3::pyclass(name = "Iterator", module = "markupever._rustlib.iter", frozen)] pub struct PyIterator { dom: Arc>, index: atomic::AtomicUsize, @@ -48,7 +48,7 @@ macro_rules! axis_iterators { ) => { $( #[$m] - #[pyo3::pyclass(name = $pyname, module = "markupever._rustlib")] + #[pyo3::pyclass(name = $pyname, module = "markupever._rustlib.iter")] pub struct $name { guard: Option, } @@ -94,7 +94,7 @@ axis_iterators! { PyLastChildren(crate::nodes::NodeGuard::last_child) as "LastChildren"; } -#[pyo3::pyclass(name = "Children", module = "markupever._rustlib")] +#[pyo3::pyclass(name = "Children", module = "markupever._rustlib.iter")] pub struct PyChildren { front: Option, back: Option, diff --git a/src/iter/mod.rs b/src/iter/mod.rs index ac41d1c..a5f8c2f 100644 --- a/src/iter/mod.rs +++ b/src/iter/mod.rs @@ -1,4 +1,4 @@ -use pyo3::types::PyModuleMethods; +use pyo3::types::{PyAnyMethods, PyModuleMethods}; mod iterator; mod traverse; @@ -20,5 +20,14 @@ pub fn register_iter_module(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3: iter_module.add_class::()?; iter_module.add_class::()?; - m.add_submodule(&iter_module) + m.add_submodule(&iter_module)?; + + // Register it like a submodule of a package, so that `import markupever._rustlib.iter` + // works. + let name = format!("{}.iter", m.name()?); + iter_module.setattr("__name__", &name)?; + m.py() + .import("sys")? + .getattr("modules")? + .set_item(name, iter_module) } diff --git a/src/iter/traverse.rs b/src/iter/traverse.rs index 47a6829..bf74589 100644 --- a/src/iter/traverse.rs +++ b/src/iter/traverse.rs @@ -4,7 +4,7 @@ enum EdgeSign { } /// An iterator which traverses a subtree. -#[pyo3::pyclass(name = "Traverse", module = "markupever._rustlib")] +#[pyo3::pyclass(name = "Traverse", module = "markupever._rustlib.iter")] pub struct PyTraverse { root: Option, edge: Option, @@ -73,7 +73,7 @@ impl PyTraverse { } /// An iterator over a node and its descendants. -#[pyo3::pyclass(name = "Descendants", module = "markupever._rustlib")] +#[pyo3::pyclass(name = "Descendants", module = "markupever._rustlib.iter")] pub struct PyDescendants(PyTraverse); #[pyo3::pymethods]