La biblioteca Python
stxt es el parser de STXT para Python,
publicado en PyPI: Python puro, sin dependencias, 3.10 o superior. Implementa las
cinco especificaciones y devuelve los mismos códigos de error que las bibliotecas
TypeScript y Java.
La API es la de los otros dos puertos, con nombres en snake_case
(getCanonicalName → get_canonical_name). No incluye línea de comandos; la del
ecosistema es stxt.
Instalación
pip install stxt
El paquete lleva anotaciones de tipo (py.typed). Solo accede al sistema de ficheros
y al entorno en los adaptadores de la resolución, que se pueden sustituir. Todo lo
público se importa del paquete raíz; la versión está en stxt.__version__.
Parsear
| Entrada | Comportamiento |
|---|---|
parse_result(text) |
Acumula todos los errores y devuelve también los nodos que ha podido construir |
parse(text) |
Lanza una ParseException en el primer error; sin errores, devuelve list[Node] |
parse_stream(lines) |
No retiene nodos ni errores; ver Streaming y límites del parser |
from stxt import Parser, ParseException
parser = Parser()
result = parser.parse_result(text)
if result.has_errors():
for error in result.get_errors():
print(f"line {error.line} [{error.code}]: {error.message}")
roots = result.get_nodes() # los nodos raíz, en orden; puede haber varios
# La forma "lanzar al primer error"
try:
nodes = parser.parse(text)
except ParseException as e:
print(e.line, e.code, e.message)
Cada error es una ParseException con line (desde 1), code y message, también
como get_line(), get_code() y get_message(). Los errores de gramática son
ValidationException, una subclase: un solo bucle recorre los dos tipos e
isinstance los distingue. Tras un error de sintaxis el parser se recupera y sigue,
y el resultado puede incluir nodos.
El árbol
Node es una clase abstracta con dos formas, y no se puede heredar de InlineNode
ni de TextNode:
| Clase | Sintaxis | Métodos propios |
|---|---|---|
InlineNode |
Nombre: valor |
get_value()/set_value(), get_children(), get_child(name), get_children_by_name(name), add_child(), remove_child(), add_inline_node(), add_text_node() |
TextNode |
Nombre >> |
get_text_lines(), set_text(), set_text_lines(), add_text_line(), clear_text() |
Métodos comunes, en Node:
| Método | Descripción |
|---|---|
get_name() |
El nombre tal como se escribió |
get_canonical_name() |
El nombre canónico (STXT-SPEC §4.3) |
get_declared_namespace() |
El namespace escrito entre paréntesis, o "" |
get_namespace() |
El namespace efectivo, heredado de los padres |
get_line() |
La línea del documento |
get_level() |
El nivel, derivado de la profundidad |
get_parent() |
El InlineNode padre, o None en la raíz |
get_text() |
El valor de un inline, o las líneas unidas de un bloque |
detach() |
Separa el nodo de su padre |
get_children() y get_text_lines() devuelven tuplas de solo lectura; el árbol se
modifica con add_child, remove_child y detach.
La forma de un nodo se distingue con isinstance. Con el documento del
tutorial:
Book (com.acme.book):
Title: Arquitectura de software moderna
Authors:
Author: María Pérez
Author: Juan García
ISBN: 978-84-123456-7-8
Published: 2025-10-01
Chapter: Introducción
Content >>
Conceptos básicos y objetivos del libro.from stxt import Parser, InlineNode, TextNode, Node
book = Parser().parse_result(text).get_nodes()[0]
book.get_name() # "Book"
book.get_canonical_name() # "book"
book.get_namespace() # "com.acme.book"
book.get_line() # 1
if isinstance(book, InlineNode):
book.get_child("Title").get_text() # "Arquitectura de software moderna"
book.get_child("title").get_name() # "Title": la búsqueda es por nombre canónico
book.get_child("Publisher") # None: no existe
authors = book.get_child("Authors")
[a.get_text() for a in authors.get_children_by_name("Author")] # ['María Pérez', 'Juan García']
authors.get_declared_namespace() # "": no lo declara...
authors.get_namespace() # "com.acme.book": ...lo hereda
content = book.get_child("Chapter").get_child("Content")
if isinstance(content, TextNode):
content.get_text_lines() # ('Conceptos básicos y objetivos del libro.',)
content.get_level() # 2
content.get_parent() is book.get_child("Chapter") # True
# Un recorrido genérico
def walk(node: Node, depth: int = 0) -> None:
print(" " * depth + node.get_name())
if isinstance(node, InlineNode):
for child in node.get_children():
walk(child, depth + 1)
Construir y modificar
Los árboles son mutables:
add_childlanzaRuntimeExceptionsi el nodo ya tiene padre (NODE_ALREADY_ATTACHED) o es un ancestro (NODE_CYCLE);remove_childydetach()lo separan.set_valueyadd_text_linerechazan un salto de línea (LINE_BREAK_NOT_ALLOWED).- El nivel se deriva de la cadena de padres, y la línea solo la fija el parser.
- En los constructores y fábricas con dos cadenas, la segunda es el contenido (valor
o texto); el namespace solo aparece en la forma de tres argumentos,
InlineNode(name, namespace, value). También valen los argumentos con nombrevalue=,namespace=ytext=.
from stxt import InlineNode, TextNode
email = InlineNode("Email", "com.example.mail", "Weekly report")
email.add_inline_node("From", "Ana García <[email protected]>")
to = email.add_inline_node("To")
to.add_inline_node("Address", "[email protected]")
body = email.add_text_node("Body", "Hi Bob,\n\nSee attached.")
body.get_parent() is email # True
body.get_level() # 1
to.get_namespace() # "com.example.mail", heredado
# Reordenar: "To" al principio
to.detach()
email.add_child(to, 0)
# Editar
email.set_namespace("com.example.docs") # todo el subárbol que hereda le sigue
body.set_text("Hi Bob,\n\nSee the new attachment.")
Validar contra un esquema o una plantilla
UnifiedSchemaProvider carga esquemas y plantillas con add_file(text): parsea la
definición, la valida contra su meta-esquema y registra el esquema por namespace.
SchemaValidator es un Validator que se registra en el Parser y valida cada nodo
con namespace al cerrarlo.
Template (@stxt.template): com.acme.book
Structure >>
Book:
Title: (1)
Authors: (1)
Author: (+)
ISBN: (1)
Publisher: (?)
Published: (?) DATE
Summary: (?) TEXT
Chapter: (+)
Content: (?) TEXT
Description >>
Book: Plantilla para fichas de libros editorialesfrom stxt import (
Parser, UnifiedSchemaProvider, SchemaValidator,
ValidationException,
)
provider = UnifiedSchemaProvider()
provider.add_file(template_text) # lanza si la plantilla no valida contra su meta-esquema
parser = Parser()
parser.register_validator(SchemaValidator(provider))
result = parser.parse_result(document_text)
for error in result.get_errors():
kind = "schema" if isinstance(error, ValidationException) else "syntax"
print(f"{kind} line {error.line} [{error.code}]: {error.message}")
Con un libro sin ISBN y con una fecha que no es YYYY-MM-DD, el bucle imprime:
schema line 6 [INVALID_VALUE]: Published: Invalid date (1 de octubre de 2025)
schema line 1 [TOO_FEW_CHILDREN]: 0 nodes of 'com.acme.book:isbn' and min is 1
- Un error de cardinalidad se señala en la línea del padre.
- Un namespace que el proveedor no conoce produce un
SCHEMA_NOT_FOUNDpor nodo; el proveedor no lanza (get_schema()devuelveNone). - Los nodos sin namespace no se validan (STXT-SCHEMA-SPEC §5). Una definición se valida siempre contra su meta-esquema.
add_fileacepta ficheros con varias definiciones;get_all_schemas()las enumera yclear()vacía el proveedor.- Están implementados todos los tipos de STXT-SCHEMA-SPEC §9.
El esquema compilado se puede inspeccionar: provider.get_schema(ns) devuelve un
Schema con get_namespace() y get_node_definition(name); cada NodeDefinition
tiene get_type(), get_children() (un diccionario de ChildDefinition por nombre
cualificado namespace:nombre, con get_min() / get_max()), get_values() para
un ENUM y get_description().
Resolución de gramáticas
DiscoveryResolver implementa STXT-DISCOVERY-SPEC: dado el
directorio de un documento, determina qué definiciones le aplican (los .stxt/ del
documento y de sus ancestros, después ~/.stxt y /etc/stxt, con precedencia por
namespace; STXT_PATH sustituye la cadena), igual que la CLI y la extensión.
El resolutor recibe un DiscoveryFileSystem y un DiscoveryEnvironment. El paquete
incluye OsDiscoveryFileSystem (sobre os) y SystemDiscoveryEnvironment
(STXT_PATH, ~/.stxt y /etc/stxt o %ProgramData%\stxt), y el atajo
stxt.discovery.resolve(document_dir) sobre ellos; un test puede pasar un árbol en
memoria. A resolve se le pasa el directorio del documento, o None para la entrada
estándar o un búfer sin guardar; en ese caso la cadena empieza en el nivel de
usuario. DiscoveryResult implementa SchemaProvider y se pasa directamente al
validador:
from stxt import Parser, SchemaValidator
from stxt.discovery import resolve
discovery = resolve("/home/ana/libros/docs")
discovery.get_chain() # ['/home/ana/libros/.stxt'] (todos los ancestros, el más cercano primero)
# Los errores de resolución se acumulan, no se lanzan
for error in discovery.get_errors():
print(f"[{error.code}] {error.file}: {error.message}")
parser = Parser()
parser.register_validator(SchemaValidator(discovery))
result = parser.parse_result(document_text)
DiscoveryResult registra también el origen de cada definición:
definition = discovery.get_definition("com.acme.book")
definition.file # '/home/ana/libros/.stxt/@stxt.template/com.acme.book.stxt'
definition.level_dir # '/home/ana/libros/.stxt' (el nivel que ha ganado)
definition.schema # el Schema compilado
discovery.get_active_definitions() # una por namespace, con la precedencia aplicada
discovery.get_all_schemas() # solo los esquemas de las anteriores
Un DiscoveryResolver propio cachea los niveles por directorio, para resolver muchos
documentos; clear_cache() los invalida cuando los ficheros de definición pueden
haber cambiado:
from stxt import DiscoveryResolver, OsDiscoveryFileSystem, SystemDiscoveryEnvironment
resolver = DiscoveryResolver(OsDiscoveryFileSystem(), SystemDiscoveryEnvironment())
discovery = resolver.resolve("/home/ana/libros/docs")
resolver.clear_cache()
Los errores son DiscoveryError (code, file, message, namespace), con los
códigos DISCOVERY_DUPLICATE_NAMESPACE, DISCOVERY_NOT_A_DEFINITION,
DISCOVERY_NOT_PARSEABLE y DISCOVERY_INVALID_DEFINITION.
El árbol canónico en JSON
to_canonical_tree(nodes) devuelve el valor JSON de
STXT-TREE-SPEC como una lista de diccionarios, el mismo árbol que
emite stxt describe, y to_canonical_json(nodes) lo serializa con dos espacios de
sangría. Emiten solo los campos normativos, sin posiciones ni comentarios.
from stxt import Parser, to_canonical_tree, to_canonical_json
nodes = Parser().parse_result(text).get_nodes()
tree = to_canonical_tree(nodes)
tree[0]["name"] # 'Book'
tree[0]["form"] # 'inline'
print(to_canonical_json(nodes))
Escribir STXT
NodeWriter serializa un nodo, o una lista de nodos raíz, en la forma canónica de
STXT-TREE-SPEC §11, con IndentStyle.TABS (por defecto) o
IndentStyle.SPACES_4. Escribe el namespace solo donde cambia respecto al padre.
Como parte del árbol, la salida no lleva comentarios ni líneas en blanco fuera de los
bloques; es lo que hace stxt format --clean.
from stxt import NodeWriter, IndentStyle
one = NodeWriter.to_stxt(email) # un nodo, con tabuladores
all = NodeWriter.to_stxt_docs(result.get_nodes(), IndentStyle.SPACES_4) # un documento entero
El email construido más arriba, escrito con to_stxt:
Email (com.example.docs): Weekly report
To:
Address: [email protected]
From: Ana García <[email protected]>
Body >>
Hi Bob,
See the new attachment.Formatter (STXT-TREE-SPEC §12) reformatea conservando
comentarios y líneas en blanco: reescribe el texto original línea a línea y devuelve,
junto al texto, los errores de sintaxis encontrados. Aplica las mismas reglas que
stxt format, la extensión y el playground.
from stxt import Formatter, IndentStyle
formatted = Formatter.format(source, IndentStyle.TABS)
text, errors = formatted.text, formatted.errors
Observar y validar durante el parseo
Observer es una clase base con cuatro callbacks vacíos, de los que se redefinen
los necesarios. El parser los llama durante el parseo: on_create(node, line_string) al abrir un nodo (ya con padre, namespace efectivo y nivel),
on_finish(node) al cerrarlo, on_comment(line_number, line_string) por cada
comentario y on_text_line(node, line_number, line_string, line_indent) por cada
línea de un bloque.
Un Validator se ejecuta sobre cada nodo al cerrarlo y devuelve una lista de
ValidationException, sin lanzar. SchemaValidator es el que trae la biblioteca.
from stxt import Parser, Observer, Node
class LoggingObserver(Observer):
def on_create(self, node: Node, line_string: str) -> None:
print("open", node.get_qualified_name())
def on_finish(self, node: Node) -> None:
print("close", node.get_qualified_name())
parser = Parser()
parser.register_observer(LoggingObserver())
parser.parse_result(text)
Streaming y límites del parser
parse_stream(lines) recibe un iterable de líneas, cada una sin su salto de línea
(por ejemplo, un fichero abierto), y no retiene nodos ni errores. Los resultados
llegan por un StreamObserver, una clase base con dos callbacks:
on_root_node(node) con cada nodo raíz completo y ya validado, que el parser libera
tras la llamada, y on_error(error) con cada error, de sintaxis o de validación. La
memoria en uso es la de un árbol raíz, lo que permite procesar ficheros que no caben
en memoria. Un StreamObserver registrado recibe las mismas llamadas con parse y
parse_result.
from stxt import Parser, StreamObserver, Node, ParseException
class Counter(StreamObserver):
def __init__(self) -> None:
self.roots = 0
def on_root_node(self, node: Node) -> None:
self.roots += 1
def on_error(self, error: ParseException) -> None:
print(error.line, error.code, error.message)
parser = Parser()
counter = Counter()
parser.register_stream_observer(counter)
with open("registro.stxt", encoding="utf-8") as f:
parser.parse_stream(line.rstrip("\n") for line in f)
counter.roots
El parser aplica los límites de STXT-SPEC §11.2, configurables
en el constructor: max_nesting (100 niveles), max_line_length (10 000 caracteres) y
max_input_size (10 000 000 caracteres); -1 desactiva uno. Superar un límite produce
una LimitException (subclase de ParseException, con los códigos
LIMIT_NESTING_EXCEEDED, LIMIT_LINE_LENGTH_EXCEEDED y LIMIT_INPUT_SIZE_EXCEEDED) y
aborta el parseo.
parser = Parser(max_nesting=50, max_input_size=-1)
La superficie de la API
Todo lo importable de stxt; los subpaquetes (stxt.schema, stxt.discovery...)
también lo son:
| Grupo | Nombres |
|---|---|
| Parseo | Parser, ParseResult, Node, InlineNode, TextNode, NO_LINE, LineIndent, parse_line, EMPTY_NAMESPACE, SPEC_VERSION |
| Errores | ParseException, ValidationException, LimitException, RuntimeException |
| Puntos de extensión | Observer, StreamObserver, Validator |
| Esquemas | Schema, NodeDefinition, ChildDefinition, SchemaProvider, SchemaProviderMemory, SchemaProviderMeta, SchemaValidator, Type, TypeRegistry, transform_node_to_schema, SCHEMA_NAMESPACE |
| Plantillas | MetaTemplateSchemaProvider, TemplateSchemaProviderMemory, transform_template_node_to_schema, TEMPLATE_NAMESPACE |
| Runtime | UnifiedSchemaProvider, NodeWriter, IndentStyle, Formatter, FormatResult, to_canonical_tree, to_canonical_json |
| Resolución | DiscoveryResolver, DiscoveryResult, DiscoveryDefinition, DiscoveryLevel, DiscoveryError, DiscoveryFileSystem, DiscoveryEntry, DiscoveryEnvironment, OsDiscoveryFileSystem, SystemDiscoveryEnvironment y stxt.discovery.resolve |
El paquete sigue el versionado semántico: la API no cambia de forma incompatible dentro de la línea 1.x. El estado de cada especificación está en Estabilidad y versiones. Los cambios de cada versión están en el repositorio, donde se reportan también los errores.