The Zuri Standard Library
Every module Zuri ships with, and every public name in each one. The pages here are generated from the library’s own doc blocks, so what you read is what the code says.
This is a reference, not a tutorial. It answers “what does this take and
what does it return”. For the narrative version, with worked examples
and the reasoning behind the design, read The Zuri Programming
Language, which cargo run-docs serves alongside this.
How to Read a Page
Every module page opens with the import that reaches it, then the
module’s own description, then its members. A package also carries an
index of its whole public surface, because a name you reach as
os.get_env may well be declared in a file called something else.
Signatures are written the way the source declares them:
name: stringis a parameter that must be a string, enforced at the call.name: ?stringaccepts a string ornil, which is how an optional argument is spelled....valuestakes any number of arguments and arrives as a list.-> stringis what the call gives back.
A name beginning with an underscore is private: the compiler refuses to import it across a module boundary, so none appear here.
The Modules
Text and Markup
Parsing, building and colouring text meant to be read.
| Module | What it is for |
|---|---|
html | A complete HTML5 parser, DOM and serializer, following the WHATWG HTML Living Standard. |
wire | Wire, an HTML template engine. |
url | This module provides classes and functions for parsing, building, resolving and processing URLs (and URL-like identifiers, such… |
mime | This module provides functions that allow easy mime type detection from files. |
colors | This module provides functionalities for color conversion and manipulation. |
Data Formats
Reading and writing the formats other programs speak.
| Module | What it is for |
|---|---|
json | Provides APIs for encoding and decoding JSON data. |
yaml | A complete, YAML 1.2.2-compliant library for parsing and emitting YAML (YAML Ain’t Markup Language) documents. |
toml | TOML, read for its values or edited in place without disturbing the file around them. |
csv | A complete, RFC 4180-compliant library for reading and writing Comma-Separated Values (CSV) data. |
base64 | This module provides interface for encoding binary data into strings and decoding such encoded strings back into binary data… |
struct | This module provides functions for converting between Zuri values and C/C++/Rust structs and vice-versa in the binary format. |
Numbers and Collections
Arithmetic, and the containers the built-in types do not cover.
| Module | What it is for |
|---|---|
math | This module contains functions and constants to make trigonometric and non-trigonometric mathematics a breeze. |
array | This module provides fixed-width typed array classes: twos- complement integers (Int8Array/UInt8Array through… |
set | This module provides functionalities for working with mathematical sets. |
Dates and Times
Calendar arithmetic, formatting, and the timezone database.
| Module | What it is for |
|---|---|
date | This modules provides Zuri’s implementation of date and time manipulation methods. |
The Machine
The filesystem, other processes, the terminal, the command line, and the configuration around them.
| Module | What it is for |
|---|---|
os | The os module is Zuri’s interface to the underlying operating system: environment variables, the filesystem, other processes,… |
env | Configuration from a file, in the environment, in the type you wanted. |
io | This module provides interfaces for working with to I/O stream and TTYs as well as expose the operating system standard I/O for… |
args | This module provides a complete, batteries-included framework for building command-line interfaces. |
log | This module implements a simple and flexible event logging system for all Zuri applications and modules. |
stat | The module provides constants and functions for interpreting results of file.stat(). |
Testing
Suites, matchers, test doubles, snapshots, and the reports they produce.
| Module | What it is for |
|---|---|
test | A testing framework: suites, assertions, mocks, snapshots, lifecycle hooks, and reports worth reading. |
Networking
Sockets, addresses, and a complete HTTP stack and JSON-RPC on top of them.
| Module | What it is for |
|---|---|
net | The net module provides networking primitives for Transmission Control Protocol and User Datagram Protocol communication as… |
http | A complete HTTP stack: a client, a server, and the pieces both are built from. |
rpc | JSON-RPC 2.0: calling methods on another program, and answering its calls, over HTTP, WebSockets, sockets, pipes, or any stream… |
Security and Identity
Hashing, signing, password storage, tokens and input validation.
| Module | What it is for |
|---|---|
crypto | Comprehensive cryptographic primitives for Zuri applications. |
hash | This module provides a framework for cryptographic and non-cryptographic encryption. |
bcrypt | Generating and verifying bcrypt password hashes, and reading information back out of an existing hash (its cost factor and salt). |
jwt | The jwt module provides a complete implementation of JSON Web Tokens (JWT) as defined in RFC 7519, with support for signing,… |
uuid | Provides RFC 9562 (and RFC 4122) compliant Universally Unique Identifier (UUID) generation, parsing, validation, and inspection. |
validate | Schema-based input validation for Zuri applications. |
Compression and Archives
Every codec and container format the runtime ships with.
| Module | What it is for |
|---|---|
compress | The compress library provides in-memory and streaming compression and decompression across several algorithms, plus… |
Concurrency
Real OS threads, with separate heaps and message passing between them.
| Module | What it is for |
|---|---|
isolate | High-throughput parallel concurrency backed by a pool of operating-system isolate threads. |
Images
Decoding, drawing, filtering and encoding raster images.
| Module | What it is for |
|---|---|
imagine | Reading, writing, drawing and transforming raster images. |
Databases
One contract every relational database answers, and the adapters that answer it.
| Module | What it is for |
|---|---|
sql | One way to talk to a relational database, whichever one it is. |
Messages, the three protocols that move them, and the servers at the far end.
| Module | What it is for |
|---|---|
mail | Mail, from the message to the socket. |
Interoperability
Calling C and Rust libraries, being called back by them, and linking static ones.
| Module | What it is for |
|---|---|
ffi | Calling C and Rust libraries, and being called by them. |
Types and Conversion
Checked coercion between types, named constants, and conversions between bases and encodings.
| Module | What it is for |
|---|---|
types | Provides type validation and conversion capabilities |
enum | This module provides Zuri’s implementation of enumerations as described in the language documentation: a set of unique values… |
convert | Data conversion between hexadecimal, arbitrary numeric bases, bytes, and decimal numbers. |
Metaprogramming
Lexing, parsing and compiling Zuri source, and reflecting on live functions, classes and modules.
| Module | What it is for |
|---|---|
zuri | Exposes Zuri’s own compiler pipeline as a library: lexing, parsing, and compiling a Zuri source file, plus a reflect submodule… |
html
import html
A complete HTML5 parser, DOM and serializer, following the WHATWG HTML Living Standard.
Parse a page, walk it, query it with CSS selectors, change it, and write
it back out. Nothing here fetches anything over the network and nothing
here executes a script; <script> content is inert text and always will
be.
Quick start
import html
var doc = html.parse('
<article>
<h1 id="title">Zuri</h1>
<ul class="links">
<li><a href="/docs">Docs</a>
<li><a href="/spec">Spec</a>
</ul>
</article>
')
echo doc.get_element_by_id('title').text_content()
for link in doc.query_selector_all('ul.links a') {
echo '${link.text_content()} -> ${link.get_attribute("href")}'
}
Notice that neither <li> is closed and there is no <html>, <head>
or <body> anywhere. That is fine: the standard defines a tree for
every possible input, and this module builds it. Parsing never raises on
malformed markup.
Finding things
| Method | Finds |
|---|---|
get_element_by_id(id) | the first element with that id |
get_elements_by_class_name(c) | every element with those classes |
get_elements_by_tag_name(t) | every element with that tag |
query_selector(sel) | the first CSS match |
query_selector_all(sel) | every CSS match |
element.matches(sel) | whether this element matches |
element.closest(sel) | this element or its nearest matching ancestor |
The selector syntax is a deliberately bounded subset: type, *, #id,
.class, [attr], [attr="value"], the combinators >, + and ~,
descendant combination by whitespace, :first-child, :last-child,
:nth-child(), and comma-separated lists of all of those. Anything
outside it raises a SelectorError rather than silently matching
nothing.
Changing things
import html
var doc = html.parse('<div class="box"><p>old</p></div>')
var box = doc.query_selector('.box')
box.set_attribute('data-seen', 'yes')
box.class_list().add('active')
box.set_inner_html('<p>new</p>')
echo box.outer_html()
# <div class="box active" data-seen="yes"><p>new</p></div>
Zuri has no property setters, so every writable DOM property is a pair
of methods: text_content() reads and set_text_content() writes, and
likewise for inner_html and outer_html.
Writing it back out
outer_html() round-trips a node exactly. When you want the output
shaped for a particular audience, minify() strips what a browser does
not need and format() indents for a human:
A string is parsed as a whole document, so <html>, <head> and
<body> show up in the output the way a browser would produce them.
Pass { fragment: true } when the input is a fragment and you want just
that fragment back:
%> import html
%> html.minify('<p> a b </p>\\n<p>c</p>', { fragment: true })
'<p>a b</p><p>c</p>'
%> echo html.format('<ul><li>a<li>b</ul>', { fragment: true })
<ul>
<li>a</li>
<li>b</li>
</ul>
Escaping
encode() makes text safe to embed and decode() resolves character
references. Decoding knows all 2231 named references, not a shortlist:
%> import html
%> html.encode('<b>Tom & Jerry</b>')
'<b>Tom & Jerry</b>'
%> html.decode('café — ☃')
'café: ☃'
Encoding
Input is UTF-8 text that has already been decoded, which is what a Zuri
string always is. There is no charset sniffing, no byte order mark
handling and no <meta charset> processing: if you are reading bytes
off a socket, decode them before they get here.
Parse errors
HTML has no fatal errors, so a parse always succeeds. Every conformance problem the standard defines is collected instead:
%> import html
%> var doc = html.parse('<p><b>x</p>')
%> for e in doc.errors { echo e.to_string() }
missing-doctype at 1:1
unexpected-end-tag at 1:8
Each entry carries the standard’s own error name, so you can look it up, plus the line and column where it was noticed.
The html API
Every public name in html, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
html.ClassList | class | A live view of one element’s class attribute. |
html.Comment | class | An HTML comment. |
html.Document | class | A whole parsed document. |
html.DocumentFragment | class | A parentless container for a run of nodes. |
html.DocumentType | class | The <!DOCTYPE ...> node at the top of a document. |
html.Element | class | An element in the tree. |
html.HTML_NAMESPACE | constant | Namespace URI of ordinary HTML elements. |
html.HierarchyError | class | Raised when a tree mutation would produce a structure that cannot exist, such as inserting a node before… |
html.MATHML_NAMESPACE | constant | Namespace URI of elements inside a <math> subtree. |
html.NODE_COMMENT | constant | Node type of a Comment. |
html.NODE_DOCUMENT | constant | Node type of a Document. |
html.NODE_DOCUMENT_FRAGMENT | constant | Node type of a DocumentFragment, including the fragment that holds a <template> element’s content. |
html.NODE_DOCUMENT_TYPE | constant | Node type of a DocumentType (the <!DOCTYPE ...> node). |
html.NODE_ELEMENT | constant | Node type of an Element. |
html.NODE_TEXT | constant | Node type of a Text node. |
html.Node | class | The base class every node in a parsed document inherits from. |
html.ParseError | class | A parse error raised while tokenizing or building the tree. |
html.RAW_TEXT_ELEMENTS | constant | Elements whose text children are markup, not content: their text is written out byte for byte and never… |
html.SVG_NAMESPACE | constant | Namespace URI of elements inside an <svg> subtree. |
html.Selector | class | A compiled selector list: h1, h2 is one of these holding two ComplexSelector instances. |
html.SelectorError | class | Raised when a selector cannot be parsed, or uses syntax this module deliberately does not support. |
html.Text | class | A run of character data in the tree. |
html.Token | class | One token from the tokenizer. |
html.Tokenizer | class | Turns markup into tokens, one call to next_token() at a time. |
html.TreeBuilder | class | Builds a document tree from a token stream. |
html.VOID_ELEMENTS | constant | The HTML elements that are written without a closing tag and can hold no content. |
html.XLINK_NAMESPACE | constant | Namespace URI used by the xlink: attribute prefix in SVG. |
html.XMLNS_NAMESPACE | constant | Namespace URI used by the xmlns and xmlns:xlink attributes. |
html.XML_NAMESPACE | constant | Namespace URI used by the xml: attribute prefix. |
html.compile | function | Compiles source into a reusable Selector. |
html.decode | function | Decodes every character reference in text and returns the result. |
html.elements.FOREIGN_ATTRIBUTES | constant | Attributes in foreign content that belong to a namespace, mapping the lowercase name the tokenizer produced… |
html.elements.FOREIGN_BREAKOUT_TAGS | constant | Start tags that are always a mistake inside foreign content and that break out of it, closing SVG or MathML… |
html.elements.FORMATTING_ELEMENTS | constant | The formatting elements: the ones the adoption agency algorithm reopens across a badly nested boundary, so… |
html.elements.HTML4_TRANSITIONAL_PREFIXES | constant | Public identifier prefixes whose mode depends on whether the doctype also carries a system identifier: quirks… |
html.elements.IMPLIED_END_TAGS | constant | Elements whose end tag is implied by the start of a sibling, so that <li>a<li>b produces two list items… |
html.elements.LIMITED_QUIRKS_PUBLIC_PREFIXES | constant | Public identifier prefixes that always mean limited-quirks mode. |
html.elements.MATHML_ATTRIBUTES | constant | The one MathML attribute whose case the tokenizer destroys. |
html.elements.MATHML_TEXT_INTEGRATION_POINTS | constant | The MathML elements whose children are parsed as HTML rather than as MathML. |
html.elements.QUIRKS_PUBLIC_EXACT | constant | The two public identifiers that put a document in quirks mode on an exact match rather than a prefix match,… |
html.elements.QUIRKS_PUBLIC_PREFIXES | constant | Public identifier prefixes that put a document in quirks mode, in lowercase for case-insensitive comparison. |
html.elements.QUIRKS_SYSTEM_ID | constant | The system identifier that alone puts a document in quirks mode, in lowercase. |
html.elements.SCOPE_HTML | constant | The HTML elements that make up the “in scope” barrier every scope check shares. |
html.elements.SPECIAL_HTML | constant | The HTML elements the standard calls “special”: the ones a list item or definition term stops searching past,… |
html.elements.SPECIAL_MATHML | constant | The MathML elements that count as special. |
html.elements.SPECIAL_SVG | constant | The SVG elements that count as special. |
html.elements.SVG_ATTRIBUTES | constant | SVG attribute names the tokenizer lowercased and that have to be put back, keyed by the lowercase form. |
html.elements.SVG_HTML_INTEGRATION_POINTS | constant | The SVG elements whose children are parsed as HTML rather than as SVG. |
html.elements.SVG_TAG_NAMES | constant | SVG tag names the tokenizer lowercased and that have to be put back, keyed by the lowercase form. |
html.elements.THOROUGH_IMPLIED_END_TAGS | constant | Everything in IMPLIED_END_TAGS plus the elements only closed when the standard says to generate implied end… |
html.elements.adjust_mathml_attributes | function | Repairs the case of MathML attribute names in attributes and returns a new dictionary. |
html.elements.adjust_svg_attributes | function | Repairs the case of SVG attribute names in attributes and returns a new dictionary. |
html.elements.adjust_svg_tag_name | function | The correctly cased SVG tag name for the lowercase name the tokenizer produced, or name itself when it… |
html.elements.doctype_mode | function | Which quirks mode a doctype puts a document in. |
html.elements.is_html_integration_point | function | True when element is an HTML integration point. |
html.elements.is_mathml_text_integration_point | function | True when element is a MathML text integration point, meaning its children are parsed as HTML. |
html.elements.is_special | function | True when the element name in namespace namespace is one of the standard’s special elements. |
html.encode | function | Escapes text so that it can be embedded in an HTML document without being reinterpreted as markup. |
html.entities.NO_BREAK_SPACE | constant | |
html.entities.REPLACEMENT_CHARACTER | constant | |
html.entities.code_point_to_string | function | Applies the standard’s “numeric character reference end state” rules to a raw code point and returns the… |
html.entities.consume_reference | function | Consumes a character reference from chars beginning at the ampersand at index start. |
html.entities.is_ascii_alphanumeric | function | Returns true when c is one of 0-9, A-Z or a-z. |
html.entities.is_ascii_digit | function | Returns true when c is an ASCII decimal digit. |
html.entities.is_ascii_hex_digit | function | Returns true when c is an ASCII hexadecimal digit, in either case. |
html.escape_attribute | function | Escapes value the way the HTML fragment serialization algorithm requires for a double-quoted attribute… |
html.escape_text | function | Escapes text the way the HTML fragment serialization algorithm requires for element content. |
html.format | function | Renders source as indented, readable HTML. |
html.minify | function | Removes the markup a browser does not need and returns the result. |
html.parse | function | Parses source as a complete HTML document. |
html.parse_file | function | Reads the file at path and parses it as HTML. |
html.parse_fragment | function | Parses source as a fragment, as though it had been written inside context. |
html.parser.FormattingEntry | class | One entry in the list of active formatting elements. |
html.selector.AttributeTest | class | One name/=value test from a [...] block. |
html.selector.ComplexSelector | class | One selector from a comma-separated list: a chain of compounds joined by combinators, such as ul > li + li. |
html.selector.CompoundSelector | class | A run of simple selectors with nothing between them, such as div#main.active[data-x]:first-child. |
html.selector.PseudoTest | class | One :pseudo or :pseudo(...) test. |
html.serialize.INLINE_ELEMENTS | constant | Elements that flow inside a line of text rather than starting a new block. |
html.serialize.PRESERVE_WHITESPACE | constant | Elements whose text content is significant to the last character. |
html.tokenize | function | Tokenizes source and returns every token, ending with the eof token. |
html.tokenizer.RAWTEXT_ELEMENTS | constant | Tag names whose content is text with neither markup nor character references. |
html.tokenizer.RCDATA_ELEMENTS | constant | Tag names whose content is text with character references but no markup. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
html.elements | html.elements.* | The element tables the tree construction algorithm consults on almost every token: which elements are… |
html.entities | html.entities.* | Character reference handling for HTML: turning text into something safe to drop into a document (encode()),… |
html.namespaces | html.* | The five namespace URIs the HTML parser deals in. |
html.node | html.* | The document tree that html.parse() produces, and everything you can do with it: walking it, querying it,… |
html.parser | html.parser.* | The HTML tree construction stage: the half of the parser that takes the tokenizer’s stream and decides what… |
html.selector | html.selector.* | A small, deliberately bounded CSS selector engine: enough to find things in a parsed document, and no more. |
html.serialize | html.serialize.* | Writing a document back out, shaped for whoever has to read it next: minify() for a browser, format() for… |
html.tokenizer | html.tokenizer.* | The HTML tokenizer from the WHATWG HTML Living Standard: the stage that turns a string of markup into a… |
2026, Richard Ore and The Zuri Contributors
html.elements
import html
htmlexposes this ashtml.elements, soimport htmlis enough and the names are called ashtml.elements.*.import html.elementsreaches the same definitions directly.
The element tables the tree construction algorithm consults on almost every token: which elements are “special”, which are formatting elements, which close which, and how SVG and MathML tag and attribute names have to be repaired after the tokenizer has lowercased them.
These are data, lifted from the WHATWG HTML Living Standard. They live
apart from html.parser so the algorithm there reads as the algorithm
and not as a wall of lists.
Constants
SPECIAL_HTML
html.elements.SPECIAL_HTML = [...]
The HTML elements the standard calls “special”: the ones a list item or definition term stops searching past, and the ones the adoption agency algorithm treats as a block boundary when it looks for the furthest block.
Nothing to do with CSS. This is purely the tree construction algorithm’s own category.
SPECIAL_MATHML
html.elements.SPECIAL_MATHML = [...]
The MathML elements that count as special.
SPECIAL_SVG
html.elements.SPECIAL_SVG = [...]
The SVG elements that count as special.
FORMATTING_ELEMENTS
html.elements.FORMATTING_ELEMENTS = [...]
The formatting elements: the ones the adoption agency algorithm reopens
across a badly nested boundary, so that <b>a<p>b</b>c puts a fresh
<b> inside the paragraph.
SCOPE_HTML
html.elements.SCOPE_HTML = [...]
The HTML elements that make up the “in scope” barrier every scope check shares. The narrower checks (list item scope, button scope) add to this list rather than replacing it.
IMPLIED_END_TAGS
html.elements.IMPLIED_END_TAGS = [...]
Elements whose end tag is implied by the start of a sibling, so that
<li>a<li>b produces two list items rather than nesting.
THOROUGH_IMPLIED_END_TAGS
html.elements.THOROUGH_IMPLIED_END_TAGS = [...]
Everything in IMPLIED_END_TAGS plus the elements
only closed when the standard says to generate implied end tags
thoroughly, which is what </template> and the table sections do.
MATHML_TEXT_INTEGRATION_POINTS
html.elements.MATHML_TEXT_INTEGRATION_POINTS = [...]
The MathML elements whose children are parsed as HTML rather than as MathML.
SVG_HTML_INTEGRATION_POINTS
html.elements.SVG_HTML_INTEGRATION_POINTS = [...]
The SVG elements whose children are parsed as HTML rather than as SVG.
FOREIGN_BREAKOUT_TAGS
html.elements.FOREIGN_BREAKOUT_TAGS = [...]
Start tags that are always a mistake inside foreign content and that break out of it, closing SVG or MathML elements until an HTML insertion point is reached.
SVG_TAG_NAMES
html.elements.SVG_TAG_NAMES = {...}
SVG tag names the tokenizer lowercased and that have to be put back,
keyed by the lowercase form. SVG is case-sensitive and <foreignobject>
is not the same element as <foreignObject>.
SVG_ATTRIBUTES
html.elements.SVG_ATTRIBUTES = {...}
SVG attribute names the tokenizer lowercased and that have to be put back, keyed by the lowercase form.
MATHML_ATTRIBUTES
html.elements.MATHML_ATTRIBUTES = {...}
The one MathML attribute whose case the tokenizer destroys.
FOREIGN_ATTRIBUTES
html.elements.FOREIGN_ATTRIBUTES = {...}
Attributes in foreign content that belong to a namespace, mapping the lowercase name the tokenizer produced to the qualified name and the namespace URI it should carry.
QUIRKS_PUBLIC_PREFIXES
html.elements.QUIRKS_PUBLIC_PREFIXES = [...]
Public identifier prefixes that put a document in quirks mode, in lowercase for case-insensitive comparison.
QUIRKS_PUBLIC_EXACT
html.elements.QUIRKS_PUBLIC_EXACT = [...]
The two public identifiers that put a document in quirks mode on an exact match rather than a prefix match, in lowercase.
LIMITED_QUIRKS_PUBLIC_PREFIXES
html.elements.LIMITED_QUIRKS_PUBLIC_PREFIXES = [...]
Public identifier prefixes that always mean limited-quirks mode.
HTML4_TRANSITIONAL_PREFIXES
html.elements.HTML4_TRANSITIONAL_PREFIXES = [...]
Public identifier prefixes whose mode depends on whether the doctype also carries a system identifier: quirks without one, limited-quirks with one.
QUIRKS_SYSTEM_ID
html.elements.QUIRKS_SYSTEM_ID
The system identifier that alone puts a document in quirks mode, in lowercase.
Functions
is_special()
html.elements.is_special(namespace: string, name: string) -> bool
True when the element name in namespace namespace is one of the standard’s special elements.
Parameters
namespace(string)name(string)
Returns bool
is_mathml_text_integration_point()
html.elements.is_mathml_text_integration_point(element) -> bool
True when element is a MathML text integration point, meaning its children are parsed as HTML.
Parameters
element(any)
Returns bool
is_html_integration_point()
html.elements.is_html_integration_point(element) -> bool
True when element is an HTML integration point.
An <annotation-xml> qualifies only when its encoding attribute names
an HTML flavour, which is how MathML embeds real markup.
Parameters
element(any)
Returns bool
adjust_svg_tag_name()
html.elements.adjust_svg_tag_name(name: string) -> string
The correctly cased SVG tag name for the lowercase name the tokenizer produced, or name itself when it needs no repair.
Parameters
name(string)
Returns string
adjust_svg_attributes()
html.elements.adjust_svg_attributes(attributes: dict) -> dict
Repairs the case of SVG attribute names in attributes and returns a new dictionary. Attributes that need no repair are copied across untouched, in their original order.
Parameters
attributes(dict)
Returns dict
adjust_mathml_attributes()
html.elements.adjust_mathml_attributes(attributes: dict) -> dict
Repairs the case of MathML attribute names in attributes and returns a new dictionary.
Parameters
attributes(dict)
Returns dict
doctype_mode()
html.elements.doctype_mode(name: ?string, public_id: ?string, system_id: ?string, force_quirks: bool) -> string
Which quirks mode a doctype puts a document in.
name may be nil for a doctype with no name at all, and the two
identifiers may be nil when the doctype omits them; all three cases
mean something different to the algorithm, which is why they are not
folded into empty strings.
Returns 'no-quirks', 'limited-quirks' or 'quirks'. Nothing in this
module behaves differently between them, but knowing which one a browser
would have picked is exactly the question you are asking when you audit
a legacy document.
Parameters
name(?string)public_id(?string)system_id(?string)force_quirks(bool)
Returns string
2026, Richard Ore and The Zuri Contributors
html.entities
import html.entities
htmllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhtml.entities.*needsimport html.entities.
Character reference handling for HTML: turning text into something safe
to drop into a document (encode()), and turning a document’s
&-style references back into the characters they stand for
(decode()).
Both directions follow the WHATWG HTML Living Standard. Decoding
understands all 2231 named references, decimal references (©),
hexadecimal references (©), the Windows-1252 substitutions the
standard mandates for code points in the C1 range, and the awkward
legacy rule that lets a handful of names work without their trailing
semicolon.
%> import html
%> html.decode('café — ☃')
'café: ☃'
%> html.encode('<a href="x">Tom & Jerry</a>')
'<a href="x">Tom & Jerry</a>'
Constants
REPLACEMENT_CHARACTER
html.entities.REPLACEMENT_CHARACTER = '�'
NO_BREAK_SPACE
html.entities.NO_BREAK_SPACE = ' '
Functions
is_ascii_alphanumeric()
html.entities.is_ascii_alphanumeric(c: string) -> bool
Returns true when c is one of 0-9, A-Z or a-z.
Named references are made up entirely of these, so this is the test that bounds how far reference matching will scan.
Parameters
c(string)
Returns bool
is_ascii_digit()
html.entities.is_ascii_digit(c: string) -> bool
Returns true when c is an ASCII decimal digit.
Parameters
c(string)
Returns bool
is_ascii_hex_digit()
html.entities.is_ascii_hex_digit(c: string) -> bool
Returns true when c is an ASCII hexadecimal digit, in either case.
Parameters
c(string)
Returns bool
code_point_to_string()
html.entities.code_point_to_string(code: number) -> string
Applies the standard’s “numeric character reference end state” rules to a raw code point and returns the string it should decode to.
Null, out-of-range and surrogate code points become U+FFFD. Code points in the C1 range are remapped through Windows-1252. Noncharacters and control characters are parse errors but decode to themselves, which is what browsers do and what round-tripping malformed documents depends on.
Parameters
code(number)
Returns string
consume_reference()
html.entities.consume_reference(chars: list, start: number, in_attribute: bool) -> dict
Consumes a character reference from chars beginning at the ampersand at index start.
This is the standard’s “character reference state” as a single call,
shared by decode() and by the tokenizer so both agree on every edge
case. It always succeeds: when the text at start is not a valid
reference the ampersand is returned as an ordinary character and next
points just past it.
in_attribute selects the attribute-value flavour of the algorithm.
Inside an attribute a semicolon-less legacy reference followed by = or
by an alphanumeric is left alone, so that a query string like
?x=1¬it=2 keeps its literal ¬ rather than turning into ¬it.
The returned dictionary has:
text: the decoded text (nevernil, never empty)next: index of the first character not consumederror:nil, or a short description of the parse error the standard raises for this input; the text is still produced
Parameters
chars(list)start(number)in_attribute(bool)
Returns dict
encode()
html.encode(text: string, options: ?dict) -> string
Escapes text so that it can be embedded in an HTML document without being reinterpreted as markup.
By default this escapes &, <, >, " and ', which is safe for
both element content and quoted attribute values. Everything else is
passed through unchanged, so the result stays readable and stays valid
UTF-8.
options is an optional dictionary:
quotes(bool, defaulttrue): escape"and'. Turn this off only when the result is going into element content, never into an attribute.non_ascii(bool, defaultfalse): also escape every character above U+007F. Useful when the output has to survive a transport that is not UTF-8 clean.named(bool, defaulttrue): when a character being escaped has a named reference, use it. Withfalse, non-syntactic characters use hexadecimal numeric references instead. The five syntax characters above always use their fixed forms.
%> import html
%> html.encode('5 > 3 & "quoted"')
'5 > 3 & "quoted"'
%> html.encode("it's fine", { quotes: false })
"it's fine"
%> html.encode('café', { non_ascii: true })
'café'
%> html.encode('café', { non_ascii: true, named: false })
'café'
Surrogate code points cannot appear in a Zuri string, so no unpaired-surrogate handling is needed here.
Parameters
text(string)options(?dict)
Returns string
decode()
html.decode(text: string, in_attribute: ?bool) -> string
Decodes every character reference in text and returns the result.
Named, decimal and hexadecimal references are all understood. Text that
merely looks like a reference is left exactly as it was, so decode()
is safe to run over arbitrary content: 'a & b' stays 'a & b' and
'&nosuchthing;' stays '&nosuchthing;'.
Pass true for in_attribute when the text came from an attribute
value. That switches on the standard’s attribute rule, under which a
legacy reference written without its semicolon is not decoded if the
next character is = or alphanumeric. It is what keeps ?a=1©=2
from becoming ?a=1©=2. The default is false, the element-content
behaviour.
%> import html
%> html.decode('<b> & © © ')
'<b> & © © '
%> html.decode('?a=1©=2')
'?a=1©=2'
%> html.decode('?a=1©=2', true)
'?a=1©=2'
Parameters
text(string)in_attribute(?bool)
Returns string
escape_text()
html.escape_text(text: string) -> string
Escapes text the way the HTML fragment serialization algorithm requires for element content.
Exactly four characters change: & becomes &, U+00A0 becomes
, < becomes < and > becomes >. Quotes are left
alone because they carry no meaning in element content, and the no-break
space is escaped because it is otherwise indistinguishable from a plain
space in a source listing.
This is what outer_html() and the serializers use. For escaping text
you are about to hand to something other than this module’s own
serializer, prefer encode(), which is stricter.
Parameters
text(string)
Returns string
escape_attribute()
html.escape_attribute(value: string) -> string
Escapes value the way the HTML fragment serialization algorithm requires for a double-quoted attribute value.
Three characters change: & becomes &, U+00A0 becomes
and " becomes ". < and > are deliberately left alone; they
are ordinary characters inside a quoted attribute and escaping them
would needlessly change the value’s serialized form.
Parameters
value(string)
Returns string
2026, Richard Ore and The Zuri Contributors
html.namespaces
import html
Everything here is re-exported by
html, soimport htmlis enough and the names are called ashtml.*. Importinghtml.namespaceson its own works too and reaches the same definitions.
The five namespace URIs the HTML parser deals in.
They live in a module of their own, importing nothing, because almost
every other module in html needs them at load time and a shared leaf
is the only arrangement that has no load order to get wrong.
html.node re-exports all of these, so node.HTML_NAMESPACE and
html.HTML_NAMESPACE both work and you rarely need to name this module
directly.
Constants
HTML_NAMESPACE
html.HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'
Namespace URI of ordinary HTML elements.
SVG_NAMESPACE
html.SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
Namespace URI of elements inside an <svg> subtree.
MATHML_NAMESPACE
html.MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'
Namespace URI of elements inside a <math> subtree.
XLINK_NAMESPACE
html.XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink'
Namespace URI used by the xlink: attribute prefix in SVG.
XML_NAMESPACE
html.XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace'
Namespace URI used by the xml: attribute prefix.
XMLNS_NAMESPACE
html.XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/'
Namespace URI used by the xmlns and xmlns:xlink attributes.
2026, Richard Ore and The Zuri Contributors
html.node
import html
Everything here is re-exported by
html, soimport htmlis enough and the names are called ashtml.*. Importinghtml.nodeon its own works too and reaches the same definitions.
The document tree that html.parse() produces, and everything you can
do with it: walking it, querying it, reading and changing attributes and
content, and serializing any part of it back to markup.
The shape follows the DOM closely enough that the names are familiar, without pretending to be a browser. Where this module departs from the DOM it does so deliberately and says why in the relevant doc block; the two differences worth knowing up front are:
childrenholds every child node (elements, text and comments alike), which is what the DOM callschildNodes. Usechild_elements()when you only want elements.- There are no property setters in Zuri, so every DOM property that
can be written is a pair of methods:
text_content()reads andset_text_content()writes.
import html
var doc = html.parse('<ul><li class="a">One<li class="a">Two</ul>')
for item in doc.query_selector_all('li.a') {
echo item.text_content()
}
Constants
NODE_ELEMENT
html.NODE_ELEMENT = 1
Node type of an Element.
NODE_TEXT
html.NODE_TEXT = 3
Node type of a Text node.
NODE_COMMENT
html.NODE_COMMENT = 8
Node type of a Comment.
NODE_DOCUMENT
html.NODE_DOCUMENT = 9
Node type of a Document.
NODE_DOCUMENT_TYPE
html.NODE_DOCUMENT_TYPE = 10
Node type of a DocumentType (the <!DOCTYPE ...> node).
NODE_DOCUMENT_FRAGMENT
html.NODE_DOCUMENT_FRAGMENT = 11
Node type of a DocumentFragment, including the fragment that holds a
<template> element’s content.
VOID_ELEMENTS
html.VOID_ELEMENTS = [...]
The HTML elements that are written without a closing tag and can hold no
content. Serializing one of these emits <br> and nothing else, and any
child it somehow acquired is not written out, because markup that cannot
be read back is worse than markup that loses it.
This is the list the fragment serialization algorithm uses, which is
wider than the standard’s modern “void elements” list: it also covers
basefont, bgsound, frame, keygen and param, five legacy
elements that were dropped from the authoring list but still take no end
tag and still turn up in real documents.
RAW_TEXT_ELEMENTS
html.RAW_TEXT_ELEMENTS = [...]
Elements whose text children are markup, not content: their text is written out byte for byte and never escaped, because escaping it would change what a stylesheet or a script means.
The serialization algorithm also lists noscript here, but only when
the scripting flag is enabled. This module parses with scripting
disabled by default, and in that mode a <noscript> holds real elements
rather than text, so leaving it out is right for the default and safe
for the other case: the worst that happens to a document parsed with
scripting: true is that its <noscript> text comes back escaped,
whereas including it would let hand-built text inside a <noscript>
serialize into live markup.
Classes
HierarchyError
class html.HierarchyError < Error
Raised when a tree mutation would produce a structure that cannot exist, such as inserting a node before something that is not a child of the target, or making a node its own ancestor.
Constructor
html.HierarchyError(message)
Parameters
message(string)
Node
class html.Node
The base class every node in a parsed document inherits from.
You never construct a Node directly; parsing produces Document,
Element, Text, Comment and DocumentType instances, and this
class is where the behaviour they share lives.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
node_type | One of the NODE_* constants in this module, identifying which subclass this node actually is. | |
parent_node | The node this one hangs off, or nil for a Document and for any node that has been detached. | |
children | Every child of this node in document order: elements, text nodes and comments together. |
Constructor
html.Node(node_type: number)
Parameters
node_type(number)
Node.is_element()
html.Node.is_element() -> bool
True when this node is an Element.
Returns bool
Node.is_text()
html.Node.is_text() -> bool
True when this node is a Text node.
Returns bool
Node.is_comment()
html.Node.is_comment() -> bool
True when this node is a Comment.
Returns bool
Node.is_document()
html.Node.is_document() -> bool
True when this node is a Document.
Returns bool
Node.is_doctype()
html.Node.is_doctype() -> bool
True when this node is a DocumentType.
Returns bool
Node.is_fragment()
html.Node.is_fragment() -> bool
True when this node is a DocumentFragment.
Returns bool
Node.first_child()
html.Node.first_child() -> ?Node
The first child of this node, or nil when it has none.
Returns ?Node
Node.last_child()
html.Node.last_child() -> ?Node
The last child of this node, or nil when it has none.
Returns ?Node
Node.index_in_parent()
html.Node.index_in_parent() -> number
Position of this node among its parent’s children, or -1 when it has
no parent.
Returns number
Node.next_sibling()
html.Node.next_sibling() -> ?Node
The node immediately after this one under the same parent, or nil when
this is the last child or has no parent.
Returns ?Node
Node.previous_sibling()
html.Node.previous_sibling() -> ?Node
The node immediately before this one under the same parent, or nil
when this is the first child or has no parent.
Returns ?Node
Node.next_element_sibling()
html.Node.next_element_sibling() -> ?Element
The next sibling that is an element, skipping over text and comments, or
nil when there is none.
Returns ?Element
Node.previous_element_sibling()
html.Node.previous_element_sibling() -> ?Element
The previous sibling that is an element, skipping over text and
comments, or nil when there is none.
Returns ?Element
Node.child_elements()
html.Node.child_elements() -> list
This node’s children that are elements, in document order.
A fresh list is returned on every call, so changing it does not change the tree.
Returns list
Node.first_element_child()
html.Node.first_element_child() -> ?Element
The first child that is an element, or nil.
Returns ?Element
Node.last_element_child()
html.Node.last_element_child() -> ?Element
The last child that is an element, or nil.
Returns ?Element
Node.root()
html.Node.root() -> Node
The topmost node reachable by following parent_node, which for a
parsed document is the Document itself and for a detached subtree is
that subtree’s own root.
Returns Node
Node.has_ancestor()
html.Node.has_ancestor(other: instance) -> bool
True when other is this node or one of its ancestors.
Parameters
other(Node)
Returns bool
Node.descendants()
html.Node.descendants() -> list
Every node beneath this one, in document order, not including this node itself.
Returns list
Node.walk()
html.Node.walk(callback: function)
Calls callback once for every node beneath this one, in document order. The node is passed as the only argument.
Returning false from the callback prunes that node’s subtree; any
other return value (including nil) keeps walking. The walk reads
children as it goes, so do not restructure the tree from inside the
callback.
Parameters
callback(function)
Node.append_child()
html.Node.append_child(node: instance) -> Node
Adds node as this node’s last child, detaching it from wherever it currently lives first. Returns node.
Parameters
node(Node)
Returns Node
Raises HierarchyError when node is this node or one of its
ancestors, which would make the tree cyclic.
Node.insert_before()
html.Node.insert_before(node: instance, reference: ?instance) -> Node
Inserts node immediately before reference, which must be a child of
this node. Passing nil for reference appends. Returns node.
Parameters
node(Node)reference(?Node)
Returns Node
Raises HierarchyError when reference is not a child of this
node, or when the insertion would make the tree cyclic.
Node.remove_child()
html.Node.remove_child(node: instance) -> Node
Removes node from this node’s children and returns it. The removed node keeps its own children; only its link to this parent is broken.
Parameters
node(Node)
Returns Node
Raises HierarchyError when node is not a child of this node.
Node.replace_child()
html.Node.replace_child(replacement: instance, existing: instance) -> Node
Puts replacement where existing currently sits and returns existing, now detached.
Parameters
replacement(Node)existing(Node)
Returns Node
Raises HierarchyError when existing is not a child of this node,
or when the replacement would make the tree cyclic.
Node.detach()
html.Node.detach() -> Node
Removes this node from its parent, if it has one. Returns this node so calls can be chained.
Returns Node
Node.clear_children()
html.Node.clear_children() -> Node
Removes every child of this node. The children are detached but otherwise untouched.
Returns Node
Node.text_content()
html.Node.text_content() -> string
The concatenated text of every Text node beneath this one, in document
order. Comments and doctypes contribute nothing.
Text and Comment override this to return their own data.
Unlike the browser DOM, a Document answers this the same way an
element does rather than returning nothing; being told the text of a
document you just parsed is far more useful than being told nil.
Returns string
Node.set_text_content()
html.Node.set_text_content(value: string) -> Node
Replaces every child of this node with a single Text node holding
value. Passing an empty string just empties the node, matching the
DOM.
Parameters
value(string)
Returns Node
Node.inner_html()
html.Node.inner_html() -> string
The markup of this node’s children, serialized the way the HTML fragment serialization algorithm specifies.
Returns string
Node.set_inner_html()
html.Node.set_inner_html(source: string) -> Node
Parses source as HTML in the context of this node and replaces all of its children with the result.
The parse runs the fragment parsing algorithm with this node as the
context element, so source is interpreted exactly as it would be had
it appeared inside this element in the original document. That matters:
'<td>x' keeps its cell inside a <tr> and loses it anywhere else,
which is what a browser does too.
Nothing is executed and nothing is fetched; <script> content becomes
an inert text node.
Parameters
source(string)
Returns Node
Node.outer_html()
html.Node.outer_html() -> string
The markup of this node including its own tags.
For a Document this is the whole document; for a Text node it is the
escaped text; for a Comment it is <!--...-->.
Returns string
Node.set_outer_html()
html.Node.set_outer_html(source: string) -> list
Parses source as HTML in the context of this node’s parent and puts the result where this node currently sits.
Returns the list of nodes that replaced this one, which may be empty when source produces nothing. This node is detached either way.
Parameters
source(string)
Returns list
Raises HierarchyError when this node has no parent, since there
would be nowhere to put the result.
Node.serialize_into()
html.Node.serialize_into(out: list)
Appends this node’s markup to out, a list of string pieces the caller
is expected to join().
outer_html() is the friendly form of this and is what you normally
want. Reach for this one when you are writing a large document out in
pieces and would rather not build the whole string in memory first.
Every node class overrides it; the base implementation writes nothing.
Parameters
out(list)
Node.clone_node()
html.Node.clone_node(deep: ?bool) -> Node
A deep or shallow copy of this node, detached from any parent.
The copy is shallow by default, matching the DOM: you get the node
itself with its attributes but with no children. Pass true for deep
to copy the whole subtree.
Parameters
deep(?bool)
Returns Node
Node.get_element_by_id()
html.Node.get_element_by_id(value: string) -> ?Element
The first element beneath this node whose id attribute is value, or
nil when there is none.
Ids are compared exactly, including case: HTML does not fold the case of id values even though it folds tag names.
A document with duplicate ids is malformed but perfectly parseable, and this returns the first one in document order, which is what browsers settled on.
Parameters
value(string)
Returns ?Element
Node.get_elements_by_class_name()
html.Node.get_elements_by_class_name(names) -> list
Every element beneath this node carrying all of the classes in names, in document order.
names may be a single class name, several separated by whitespace, or a list of names; in every form an element must carry all of them to match. Class names are compared exactly, including case. An empty names matches nothing.
%> doc.get_elements_by_class_name('warning')
%> doc.get_elements_by_class_name('warning sticky')
%> doc.get_elements_by_class_name(['warning', 'sticky'])
Parameters
names(string|list)
Returns list
Node.get_elements_by_tag_name()
html.Node.get_elements_by_tag_name(name: string) -> list
Every element beneath this node whose tag name is name, in document order.
The comparison folds ASCII case, so 'DIV' and 'div' find the same
elements. Passing '*' returns every element.
Parameters
name(string)
Returns list
Node.query_selector()
html.Node.query_selector(query: string) -> ?Element
The first element beneath this node matching the CSS selector query,
in document order, or nil when nothing matches.
The supported selector syntax is described in html.selector.
Parameters
query(string)
Returns ?Element
Raises SelectorError when query is not valid.
Node.query_selector_all()
html.Node.query_selector_all(query: string) -> list
Every element beneath this node matching the CSS selector query, in document order.
Parameters
query(string)
Returns list
Raises SelectorError when query is not valid.
Node.to_string()
html.Node.to_string() -> string
Renders this node as markup, the same as outer_html().
Returns string
Text
class html.Text < Node
A run of character data in the tree.
The parser merges adjacent characters into as few Text nodes as the
tree construction algorithm allows, so a paragraph of prose is normally
one node rather than one per character.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
data | The characters this node holds, already decoded: character references were resolved during parsing, so data… |
Constructor
html.Text(data: string)
Parameters
data(string)
Text.text_content()
html.Text.text_content() -> string
The characters this node holds.
Returns string
Text.set_text_content()
html.Text.set_text_content(value: string) -> Text
Replaces this node’s characters with value.
Parameters
value(string)
Returns Text
Text.append_data()
html.Text.append_data(value: string) -> Text
Appends value to this node’s characters.
Parameters
value(string)
Returns Text
Text.serialize_into()
html.Text.serialize_into(out: list)
Parameters
out(list)
Comment
class html.Comment < Node
An HTML comment.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
data | The text between <!-- and -->, exactly as it appeared. |
Constructor
html.Comment(data: string)
Parameters
data(string)
Comment.text_content()
html.Comment.text_content() -> string
The comment’s text.
Returns string
Comment.set_text_content()
html.Comment.set_text_content(value: string) -> Comment
Replaces the comment’s text with value.
Parameters
value(string)
Returns Comment
Comment.serialize_into()
html.Comment.serialize_into(out: list)
Parameters
out(list)
DocumentType
class html.DocumentType < Node
The <!DOCTYPE ...> node at the top of a document.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
name | The doctype’s name, lowercased by the tokenizer. | |
public_id | The public identifier, or an empty string when the doctype has none. | |
system_id | The system identifier, or an empty string when the doctype has none. |
Constructor
html.DocumentType(name: string, public_id: ?string, system_id: ?string)
Parameters
name(string)public_id(?string)system_id(?string)
DocumentType.text_content()
html.DocumentType.text_content() -> string
A doctype contains no text, so this is always an empty string.
Returns string
DocumentType.serialize_into()
html.DocumentType.serialize_into(out: list)
Parameters
out(list)
DocumentFragment
class html.DocumentFragment < Node
A parentless container for a run of nodes.
Fragment parsing produces one of these, and every <template> element
owns one holding its content.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
html.DocumentFragment()
DocumentFragment.serialize_into()
html.DocumentFragment.serialize_into(out: list)
Parameters
out(list)
Document
class html.Document < Node
A whole parsed document.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
mode | Which quirks mode the doctype (or the lack of one) put this document in: 'no-quirks', 'limited-quirks' or… | |
errors | Every parse error the tokenizer and tree builder raised, in the order they happened. |
Constructor
html.Document()
Document.document_element()
html.Document.document_element() -> ?Element
The document’s root element, normally <html>, or nil for an empty
document.
Returns ?Element
Document.doctype()
html.Document.doctype() -> ?DocumentType
The document’s <!DOCTYPE> node, or nil when it has none.
Returns ?DocumentType
Document.head()
html.Document.head() -> ?Element
The document’s <head> element, or nil.
The tree construction algorithm always creates one when parsing a whole
document, even for input with no <head> tag in it, so this only
returns nil for a document that was built by hand.
Returns ?Element
Document.body()
html.Document.body() -> ?Element
The document’s <body> element, or the <frameset> that stands in for
it in a frameset document, or nil when there is neither.
Returns ?Element
Document.title()
html.Document.title() -> string
The text of the document’s <title> element, with leading and trailing
whitespace removed, or an empty string when there is no title.
Returns string
Document.serialize_into()
html.Document.serialize_into(out: list)
Parameters
out(list)
ClassList
class html.ClassList
A live view of one element’s class attribute.
Every method reads and writes the attribute itself, so a class_list
never goes stale and two of them taken from the same element always
agree.
%> var el = doc.query_selector('div')
%> el.class_list().add('active')
%> el.class_list().contains('active')
true
%> el.get_attribute('class')
'active'
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
element | The element this list reads from and writes to. |
Constructor
html.ClassList(element)
Parameters
element(Element)
ClassList.to_list()
html.ClassList.to_list() -> list
The class names currently on the element, in source order, with duplicates removed.
Returns list
ClassList.length()
html.ClassList.length() -> number
How many distinct classes the element carries.
Returns number
ClassList.contains()
html.ClassList.contains(name: string) -> bool
True when the element carries name. The comparison is exact: HTML class names are case-sensitive in standards mode.
Parameters
name(string)
Returns bool
ClassList.add()
html.ClassList.add(...names: list) -> ClassList
Adds every name given to the element’s class attribute, ignoring any it already has. Returns the list so calls can be chained.
An empty name, or one containing whitespace, is rejected: those cannot be written to a class attribute without changing what it means.
Parameters
names(...string)
Returns ClassList
Raises ArgumentError
ClassList.remove()
html.ClassList.remove(...names: list) -> ClassList
Removes every name given from the element’s class attribute, ignoring any it does not have. Returns the list.
Parameters
names(...string)
Returns ClassList
Raises ArgumentError
ClassList.toggle()
html.ClassList.toggle(name: string, force: ?bool) -> bool
Adds name when the element does not have it and removes it when it
does, then returns true when the class is present afterwards.
Passing force turns this into an unconditional add (true) or remove
(false), which is handy when the desired state is already in a
variable.
Parameters
name(string)force(?bool)
Returns bool
Raises ArgumentError
ClassList.to_string()
html.ClassList.to_string() -> string
The class attribute’s value as it would be written out.
Returns string
Element
class html.Element < Node
An element in the tree.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
tag_name | The element’s tag name. | |
namespace | The namespace this element lives in: one of HTML_NAMESPACE, SVG_NAMESPACE or MATHML_NAMESPACE. | |
attributes | The element’s attributes, in source order, as a dictionary of name to value. | |
attribute_namespaces | Namespace URIs for the few attributes that have one, keyed by the same qualified name used in attributes. | |
source_line | The line in the source where this element’s start tag began, counting from 1, or 0 for an element that was… | |
source_column | The column in the source where this element’s start tag began, counting from 1, or 0 for an element that… | |
content | For a <template> element, the DocumentFragment holding its content. |
Constructor
html.Element(tag_name: string, namespace: ?string, attributes: ?dict)
Parameters
tag_name(string)namespace(?string)attributes(?dict)
Element.is_html()
html.Element.is_html() -> bool
True when this is an HTML element, as opposed to one inside an <svg>
or <math> subtree.
Returns bool
Element.is_void()
html.Element.is_void() -> bool
True when this element is one of the HTML void elements, which have no closing tag and can hold no content.
Returns bool
Element.get_attribute()
html.Element.get_attribute(name: string) -> ?string
The value of the attribute name, or nil when the element does not
carry it.
The lookup folds ASCII case for HTML elements, so
get_attribute('HREF') and get_attribute('href') are the same
question. For foreign elements the name is matched exactly, because SVG
really does distinguish viewBox from viewbox.
Parameters
name(string)
Returns ?string
Element.has_attribute()
html.Element.has_attribute(name: string) -> bool
True when the element carries the attribute name, whatever its value.
Parameters
name(string)
Returns bool
Element.set_attribute()
html.Element.set_attribute(name: string, value: string) -> Element
Sets the attribute name to value, replacing any existing value and leaving the attribute’s position among the others unchanged. A new attribute is appended after the existing ones.
Pass an empty string for a valueless attribute such as disabled; there
is no separate “no value” state.
Parameters
name(string)value(string)
Returns Element
Raises ArgumentError when name is empty or contains a character
that cannot appear in an attribute name.
Element.remove_attribute()
html.Element.remove_attribute(name: string) -> bool
Removes the attribute name and returns true when it was actually
there.
Parameters
name(string)
Returns bool
Element.attribute_names()
html.Element.attribute_names() -> list
The attribute names this element carries, in source order.
Returns list
Element.attribute_namespace()
html.Element.attribute_namespace(name: string) -> ?string
The namespace URI of the attribute name, or nil when it has none.
Only foreign content produces namespaced attributes.
Parameters
name(string)
Returns ?string
Element.set_attribute_namespace()
html.Element.set_attribute_namespace(name: string, uri: string) -> Element
Records that the attribute name belongs to uri. Called by the parser while adjusting foreign attributes; there is no reason to call it by hand.
Parameters
name(string)uri(string)
Returns Element
Element.id()
html.Element.id() -> string
The element’s id, or an empty string when it has none.
Returns string
Element.class_name()
html.Element.class_name() -> string
The element’s class attribute verbatim, or an empty string.
Returns string
Element.class_names()
html.Element.class_names() -> list
The element’s classes as a list, in source order and without duplicates.
An element with no class attribute gives an empty list.
Returns list
Element.class_list()
html.Element.class_list() -> ClassList
A ClassList view of this element’s class attribute, for adding,
removing and toggling classes.
A fresh view is returned on every call; because it reads and writes the attribute directly there is no state to keep, and two views of the same element behave identically.
Returns ClassList
Element.matches()
html.Element.matches(query: string) -> bool
True when this element matches the CSS selector query.
Parameters
query(string)
Returns bool
Raises SelectorError when query is not valid.
Element.closest()
html.Element.closest(query: string) -> ?Element
This element, or its nearest ancestor, matching the CSS selector
query: or nil when neither this element nor any ancestor does.
The search starts at the element itself, which is what makes
el.closest('div') return el when el is a div.
Parameters
query(string)
Returns ?Element
Raises SelectorError when query is not valid.
Element.serialize_into()
html.Element.serialize_into(out: list)
Parameters
out(list)
Element.inner_html()
html.Element.inner_html() -> string
The markup of this element’s children.
A <template> answers with its content fragment rather than with its
own children, which are always empty. That matches the DOM, and it is
the only way the question has a useful answer for a template.
Returns string
Element.set_inner_html()
html.Element.set_inner_html(source: string) -> Element
Parses source and replaces this element’s children with the result.
For a <template> the result goes into the content fragment, matching
where inner_html() reads from.
Parameters
source(string)
Returns Element
Element.clone_node()
html.Element.clone_node(deep: ?bool) -> Element
A deep or shallow copy of this element. Deep-cloning a <template> also
copies its content fragment, which is otherwise not part of the
element’s children.
Parameters
deep(?bool)
Returns Element
2026, Richard Ore and The Zuri Contributors
html.parser
import html.parser
htmllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhtml.parser.*needsimport html.parser.
The HTML tree construction stage: the half of the parser that takes the tokenizer’s stream and decides what the document actually means.
This is where HTML’s famous forgiveness lives. <b>a<p>b</b>c,
<table><td>x, an unclosed <li>, a </div> with nothing to close:
none of them are errors you have to handle, because the standard defines
exactly what tree each one produces, and this module implements those
rules rather than inventing its own.
Most programs want html.parse() and never come here. TreeBuilder is
public for the cases that need the machinery itself: reading errors to
lint a document, or driving a parse whose tokenizer you control.
Functions
parse()
html.parse(source: string, options: ?dict) -> Document
Parses source as a complete HTML document.
The result is always a usable Document, however broken the input was:
HTML defines a tree for every possible string, and this follows those
rules rather than raising. Anything the standard calls a parse error is
collected in document.errors, so a non-empty list there is how you
find out the source was not conformant.
The document always has an <html> root with a <head> and a <body>,
even when the source contains none of those tags, because the tree
construction algorithm creates them.
options takes:
scripting(bool, defaultfalse): parse as though scripting were enabled. With the default,<noscript>content is parsed as real markup, which is what you want when reading a page rather than rendering it. Nothing is ever executed either way.
The source is treated as UTF-8 text that has already been decoded. No
charset sniffing, byte order mark handling or <meta charset>
processing happens; a Zuri string is already Unicode by the time it
reaches here.
%> import html
%> var doc = html.parse('<p>Hello<p>World')
%> doc.query_selector_all('p').length()
2
%> doc.body().outer_html()
'<body><p>Hello</p><p>World</p></body>'
Parameters
source(string)options(?dict)
Returns Document
parse_file()
html.parse_file(path: string, options: ?dict) -> Document
Reads the file at path and parses it as HTML.
The file is read as UTF-8. Bytes that are not valid UTF-8 are replaced with U+FFFD rather than raising, so a mislabelled file still parses into something you can inspect.
import html
var doc = html.parse_file('page.html')
echo doc.title()
Parameters
path(string)options(?dict) — The same optionsparse()takes.
Returns Document
Raises Error when the file cannot be read.
parse_fragment()
html.parse_fragment(source: string, context, options: ?dict) -> DocumentFragment
Parses source as a fragment, as though it had been written inside context.
This is the algorithm behind set_inner_html(), and the context
matters: '<td>x' keeps its cell when the context is a <tr> and loses
it anywhere else, exactly as it would in a browser.
context may be an Element or a tag name. A tag name is treated as an
HTML element with no attributes, which is the common case. Passing nil
parses in a <body> context.
The returned DocumentFragment holds the parsed nodes. Its children are
detached from any document, ready to be inserted wherever you want them.
%> import html
%> html.parse_fragment('<td>x', 'tr').inner_html()
'<td>x</td>'
%> html.parse_fragment('<td>x', 'div').inner_html()
'x'
Parameters
source(string)context(?Element|string)options(?dict) — The same optionsparse()takes.
Returns DocumentFragment
Classes
FormattingEntry
class html.parser.FormattingEntry
One entry in the list of active formatting elements.
The list holds the element and the token it was created from, because the adoption agency algorithm has to build fresh copies of an element from its original attributes long after the tag that opened it has gone.
Fields
| Field | Type | Description |
|---|---|---|
element | The element currently standing for this entry, or nil when the entry is a marker. | |
token | The start tag token the element was created from. |
Constructor
html.parser.FormattingEntry(element, token)
Parameters
element(?Element) —nilmakes this a marker.token(?Token)
FormattingEntry.is_marker()
html.parser.FormattingEntry.is_marker() -> bool
True when this entry is a marker rather than an element.
Markers are pushed when a <table>, <template>, <caption>, <td>
or an applet-like element opens, and they stop formatting elements from
leaking across that boundary.
Returns bool
TreeBuilder
class html.TreeBuilder
Builds a document tree from a token stream.
The usual way in is parse(), which wires a tokenizer to a builder and
runs it. Construct one directly when you want to watch the process:
errors accumulates every conformance problem, and the stack of open
elements is readable at any point.
Fields
| Field | Type | Description |
|---|---|---|
document | The document being built. | |
tokenizer | The tokenizer feeding this builder. | |
errors | Parse errors from both stages, in the order they happened. | |
open_elements | The stack of open elements. | |
formatting | The list of active formatting elements, holding FormattingEntry instances. | |
mode | The current insertion mode, named as the standard names it: 'initial', 'in body', 'in table text' and… | |
original_mode | Where to return to after a RAWTEXT or RCDATA element finishes. | |
template_modes | The stack of template insertion modes, one per open <template>. | |
head_element | The <head> element, once it exists. | |
form_element | The innermost open <form>, which is what makes a stray </form> close the right thing. | |
frameset_ok | Whether a <frameset> could still legally replace the body. | |
scripting | Whether to parse as though scripting were enabled. | |
fragment_context | The context element when this builder is parsing a fragment, or nil for a whole document. | |
foster_parenting | Whether insertions are currently being foster parented out of a table. | |
pending_characters | Character tokens collected by the “in table text” insertion mode before it decides whether they are legal… | |
done | Set once parsing has stopped, either at end of file or because a rule said to stop. |
Constructor
html.TreeBuilder(source, options: ?dict)
Parameters
source(Tokenizer)options(?dict) —scripting(defaultfalse).
TreeBuilder.run()
html.TreeBuilder.run() -> Document
Runs the parse to completion and returns the document.
Returns Document
TreeBuilder.current_node()
html.TreeBuilder.current_node() -> ?Element
The element the parser is currently inside, or nil when the stack is
empty.
Returns ?Element
TreeBuilder.prepare_fragment()
html.TreeBuilder.prepare_fragment(context) -> Element
Puts this builder into the state the fragment parsing algorithm starts
from, with context as the element the markup is being parsed inside,
and returns the synthetic <html> root the parsed nodes will hang off.
parse_fragment() is the friendly form of this and is what you normally
want; this is public so a caller driving the tokenizer by hand can set
the same state up.
Parameters
context(Element)
Returns Element
2026, Richard Ore and The Zuri Contributors
html.selector
import html.selector
htmllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhtml.selector.*needsimport html.selector.
A small, deliberately bounded CSS selector engine: enough to find things in a parsed document, and no more.
Supported syntax
| Form | Example |
|---|---|
| type | div |
| universal | * |
| id | #main |
| class | .warning |
| attribute presence | [disabled] |
| attribute equality | [type="text"] |
| descendant | article p |
| child | ul > li |
| next sibling | h2 + p |
| subsequent sibling | h2 ~ p |
| first child | li:first-child |
| last child | li:last-child |
| nth child | li:nth-child(2n+1) |
| selector list | h1, h2, h3 |
Compound selectors combine freely: ul.menu > li[data-id]:first-child
is valid, as is any comma-separated list of those.
Deliberately not supported
Everything else, and the parser says so rather than quietly ignoring it.
:not(), :has(), :nth-of-type(), ::before, namespace prefixes,
and the substring attribute operators (^=, $=, *=, ~=, |=) all
raise a SelectorError. A selector that silently matches nothing is far
worse to debug than one that refuses to compile.
Case sensitivity
Type selectors and attribute names fold ASCII case, matching how HTML itself treats them. Ids, class names and attribute values are compared exactly, which is what standards-mode HTML does.
Functions
compile()
html.compile(source: string) -> Selector
Compiles source into a reusable Selector.
Compiled selectors are cached by source text, so calling this with the same string repeatedly costs a dictionary lookup rather than a parse. The cache holds at most 256 selectors and is dropped wholesale when it fills, so a program that builds selector strings dynamically cannot grow it without bound.
%> import html
%> var s = html.selector.compile('ul.menu > li:first-child')
%> s.matches(element)
true
Parameters
source(string)
Returns Selector
Raises SelectorError when source is empty, malformed, or uses
syntax outside the supported subset.
Classes
SelectorError
class html.SelectorError < Error
Raised when a selector cannot be parsed, or uses syntax this module deliberately does not support. The message names the offending part of the selector.
Constructor
html.SelectorError(message)
Parameters
message(string)
AttributeTest
class html.selector.AttributeTest
One name/=value test from a [...] block.
Fields
| Field | Type | Description |
|---|---|---|
name | The attribute name, lowercased. | |
value | The value the attribute must equal, or nil when the test is a bare presence check. |
Constructor
html.selector.AttributeTest(name: string, value: ?string)
Parameters
name(string)value(?string)
AttributeTest.matches()
html.selector.AttributeTest.matches(element) -> bool
True when element satisfies this test.
Parameters
element(any)
Returns bool
PseudoTest
class html.selector.PseudoTest
One :pseudo or :pseudo(...) test.
Fields
| Field | Type | Description |
|---|---|---|
name | The pseudo-class name, lowercased and without its colon. | |
step | The a of the an+b argument, for nth-child. | |
offset | The b of the an+b argument, for nth-child. |
Constructor
html.selector.PseudoTest(name: string, step: ?number, offset: ?number)
Parameters
name(string)step(?number)offset(?number)
PseudoTest.matches()
html.selector.PseudoTest.matches(element) -> bool
True when element satisfies this test.
Parameters
element(any)
Returns bool
CompoundSelector
class html.selector.CompoundSelector
A run of simple selectors with nothing between them, such as
div#main.active[data-x]:first-child. Every part has to match the same
element.
Fields
| Field | Type | Description |
|---|---|---|
tag_name | The type selector’s tag name in lowercase, or nil when the compound has none or uses *. | |
id | The id the element must carry, or nil. | |
classes | Class names the element must all carry. | |
attributes | AttributeTest instances the element must all satisfy. | |
pseudos | PseudoTest instances the element must all satisfy. |
Constructor
html.selector.CompoundSelector()
CompoundSelector.matches()
html.selector.CompoundSelector.matches(element) -> bool
True when element satisfies every part of this compound.
Parameters
element(any)
Returns bool
ComplexSelector
class html.selector.ComplexSelector
One selector from a comma-separated list: a chain of compounds joined by
combinators, such as ul > li + li.
Fields
| Field | Type | Description |
|---|---|---|
compounds | The compounds, leftmost first. | |
combinators | How each compound relates to the one before it. |
Constructor
html.selector.ComplexSelector()
ComplexSelector.matches()
html.selector.ComplexSelector.matches(element) -> bool
True when element matches this selector.
Matching runs right to left, which is what makes a selector like body div span
cheap: the rightmost compound rejects almost every element outright, and
only survivors pay for the walk up the tree.
Parameters
element(any)
Returns bool
Selector
class html.Selector
A compiled selector list: h1, h2 is one of these holding two
ComplexSelector instances.
Instances are immutable once compiled and safe to reuse, which is why
compile() caches them.
Fields
| Field | Type | Description |
|---|---|---|
source | The selector text this was compiled from, kept for error messages and for to_string(). | |
alternatives | The alternatives, in the order they were written. |
Constructor
html.Selector(source: string, alternatives: list)
Parameters
source(string)alternatives(list)
Selector.matches()
html.Selector.matches(element) -> bool
True when element matches any alternative in this selector.
Parameters
element(any)
Returns bool
Selector.to_string()
html.Selector.to_string() -> string
The selector text this was compiled from.
Returns string
2026, Richard Ore and The Zuri Contributors
html.serialize
import html.serialize
htmllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhtml.serialize.*needsimport html.serialize.
Writing a document back out, shaped for whoever has to read it next:
minify() for a browser, format() for a person.
Neither of these is the same as outer_html(). That one is exact and
round-trips a tree byte for byte; these two deliberately change
whitespace, which is the whole point of both.
%> import html
%> html.minify('<p> a b </p>\n<p>c</p>', { fragment: true })
'<p>a b</p><p>c</p>'
%> echo html.format('<ul><li>a<li>b</ul>', { fragment: true })
<ul>
<li>a</li>
<li>b</li>
</ul>
Constants
INLINE_ELEMENTS
html.serialize.INLINE_ELEMENTS = [...]
Elements that flow inside a line of text rather than starting a new block. Whitespace around these carries meaning, so neither serializer moves it.
This is the default rendering of each element. CSS can turn any
element into any box type, and neither serializer reads CSS, so a
stylesheet that makes a <div> inline is a case where format() can
change how the page looks. minify() is safe either way: it only ever
collapses runs of whitespace to a single space, which both box types
render identically.
PRESERVE_WHITESPACE
html.serialize.PRESERVE_WHITESPACE = [...]
Elements whose text content is significant to the last character. Nothing inside one of these is ever reflowed, re-indented or collapsed.
Functions
minify()
html.minify(source, options: ?dict) -> string
Removes the markup a browser does not need and returns the result.
source is either a node you already have or a string of HTML. A string
is parsed as a complete document, so <html>, <head> and <body>
appear in the output even when the input had none of them, exactly as a
browser would produce them. Pass { fragment: true } to parse and
return just the markup you gave it instead.
What this changes:
- Runs of whitespace in text become a single space.
- Whitespace-only text between two block level elements is removed entirely, along with leading and trailing whitespace inside a block level element.
- Comments are removed.
What this never touches:
- The content of
<pre>,<textarea>,<script>,<style>,<xmp>,<listing>and<plaintext>. - Whitespace next to an inline element, which is a word separator and cannot be removed without changing the text.
- Attribute values, attribute order, tag names, or the doctype.
options:
fragment(bool, defaultfalse): parse a string source as a fragment rather than a whole document. Ignored when source is already a node.comments(bool, defaultfalse): keep comments. Turn this on when the markup carries conditional comments or a licence header that has to survive.collapse_whitespace(bool, defaulttrue): do the whitespace work at all. Withfalsethe only thing minifying does is drop comments.
The tree passed in is never modified; the work happens on a copy.
%> import html
%> html.minify('<div> <p>a b</p> <!-- x --> </div>', { fragment: true })
'<div><p>a b</p></div>'
%> html.minify('<p>a <b> b </b> c</p>', { fragment: true })
'<p>a <b> b </b> c</p>'
Parameters
source(Node|string)options(?dict)
Returns string
format()
html.format(source, options: ?dict) -> string
Renders source as indented, readable HTML.
This is a pretty printer, not a round-trip. It moves whitespace between
elements to make the structure visible, so the output is equivalent for
reading and reviewing but is not byte-identical to the input. Use
outer_html() when the exact markup matters.
Whitespace inside inline content is left alone, because collapsing it
would change the rendered text; only the gaps between block level
elements are re-indented. The content of <pre>, <textarea>,
<script>, <style>, <xmp>, <listing> and <plaintext> is copied
through untouched.
options:
indent(number or string, default2): a number is that many spaces per level; a string is used as the unit verbatim, so'\t'gives tab indentation.fragment(bool, defaultfalse): parse a string source as a fragment rather than a whole document.comments(bool, defaulttrue): keep comments.
%> import html
%> echo html.format('<div><p>Hi <b>there</b></p></div>', { fragment: true })
<div>
<p>Hi <b>there</b></p>
</div>
%> echo html.format('<div><p>x</p></div>', { fragment: true, indent: '\t' })
<div>
<p>x</p>
</div>
Parameters
source(Node|string)options(?dict)
Returns string
2026, Richard Ore and The Zuri Contributors
html.tokenizer
import html.tokenizer
htmllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhtml.tokenizer.*needsimport html.tokenizer.
The HTML tokenizer from the WHATWG HTML Living Standard: the stage that turns a string of markup into a stream of doctype, tag, comment and character tokens.
You rarely need this directly. html.parse() drives it to build a
document tree, and that is what almost every program wants. Reach for
the tokenizer when you care about the markup as written: linting a
template, rewriting attributes in place, or checking which parse errors
a document raises.
import html
for token in html.tokenize('<p class="x">hi</p>') {
echo '${token.type} ${token.name}${token.data}'
}
# start-tag p
# character hi
# end-tag p
# eof
Two deliberate departures from a literal reading of the spec
The standard emits one character token per character. This tokenizer emits one per contiguous run, so a paragraph of prose is a single token rather than a few hundred. Nothing observable changes: the tree builder splits runs back apart wherever the algorithm is defined per character.
The standard also spells the “character reference” sub-machine as nine
separate tokenizer states. Here that algorithm lives in
html.entities.consume_reference() and is shared with html.decode(),
so both agree on every edge case by construction rather than by careful
duplication.
Constants
RCDATA_ELEMENTS
html.tokenizer.RCDATA_ELEMENTS = [...]
Tag names whose content is text with character references but no markup.
A <title> may contain & but never a nested element.
RAWTEXT_ELEMENTS
html.tokenizer.RAWTEXT_ELEMENTS = [...]
Tag names whose content is text with neither markup nor character
references. <noscript> joins this list only when the tokenizer is
created with scripting enabled.
Functions
tokenize()
html.tokenize(source: string, options: ?dict) -> list
Tokenizes source and returns every token, ending with the eof token.
This is the whole-document convenience over Tokenizer: it runs with
auto_content_state on, so <script>, <style>, <title>,
<textarea> and <plaintext> have their content tokenized as text
exactly as a browser would, without a tree builder being involved.
%> import html
%> html.tokenize('<b>hi</b>')
[<start-tag b>, <character 'hi'>, <end-tag b>, <eof>]
options takes scripting (default false), which decides whether
<noscript> content is text or markup.
Parameters
source(string)options(?dict)
Returns list
Classes
Token
class html.Token
One token from the tokenizer.
A single class covers all five token types rather than five classes,
because the tree builder dispatches on type on every token and an
inheritance check would be the hottest thing in the parser. Which fields
carry meaning depends on type:
type | fields that matter |
|---|---|
doctype | name, public_id, system_id, force_quirks |
start-tag | name, attributes, self_closing |
end-tag | name, attributes, self_closing |
comment | data |
character | data |
eof | none |
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
type | One of 'doctype', 'start-tag', 'end-tag', 'comment', 'character' or 'eof'. | |
name | The tag or doctype name, lowercased. | |
data | Character data for a character token, or the text between <!-- and --> for a comment. | |
attributes | A tag’s attributes in source order, as a dictionary of lowercased name to value. | |
self_closing | True when the tag was written with a trailing slash, as in . | |
acknowledged_self_closing | Set by the tree builder when it has taken self_closing into account, which only a void or foreign element… | |
public_id | A doctype’s public identifier, or nil when it has none. | |
system_id | A doctype’s system identifier, or nil when it has none. | |
force_quirks | Set on a doctype the tokenizer could not read properly. | |
line | 1-based line the token started on. | |
column | 1-based column the token started at. |
Constructor
html.Token(type: string)
Parameters
type(string)
Token.is_start()
html.Token.is_start(name: string) -> bool
True when this token is a start tag named name.
Parameters
name(string)
Returns bool
Token.is_end()
html.Token.is_end(name: string) -> bool
True when this token is an end tag named name.
Parameters
name(string)
Returns bool
Token.to_string()
html.Token.to_string() -> string
A short, readable rendering of the token, meant for debugging and for test output rather than for round-tripping to markup.
Returns string
ParseError
class html.ParseError
A parse error raised while tokenizing or building the tree.
HTML has no fatal errors: every one of these is recoverable and the parse always produces a usable result. They are collected so that a linter, or anyone auditing markup, can see what a browser silently forgave.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
code | The standard’s name for this error, such as 'unexpected-null-character' or 'eof-in-tag'. | |
line | 1-based line the error was noticed on. | |
column | 1-based column the error was noticed at. |
Constructor
html.ParseError(code: string, line: number, column: number)
Parameters
code(string)line(number)column(number)
ParseError.to_string()
html.ParseError.to_string() -> string
Returns string
Tokenizer
class html.Tokenizer
Turns markup into tokens, one call to next_token() at a time.
The tokenizer’s state is not entirely its own: which state it should be in after a start tag depends on what the tree builder decided to do with that tag. Two ways to bridge that:
- Driven by
html.parse(), the tree builder callsset_state()itself, exactly as the standard describes. - Used on its own (which is what
tokenize()does), the tokenizer switches itself on<script>,<style>,<title>,<textarea>,<plaintext>and friends, which is what the tree builder would have told it to do anyway.
The second behaviour is controlled by the auto_content_state option
and is on by default, so a tokenizer you create by hand does the useful
thing without extra ceremony.
Fields
| Field | Type | Description |
|---|---|---|
input | The source, split into characters, with newlines already normalized: CRLF and lone CR both become LF, as the… | |
pos | How far into input the tokenizer has read. | |
state | The state machine’s current state, named exactly as the standard names it but in snake case: 'data',… | |
errors | Parse errors seen so far, as ParseError instances, in the order they happened. | |
scripting | Whether to enter RAWTEXT for <noscript>, and generally to behave as a browser with scripting turned on… | |
auto_content_state | Whether the tokenizer switches itself into RCDATA, RAWTEXT, script data or PLAINTEXT after the corresponding… | |
cdata_ok | Set by the tree builder when the insertion point is inside foreign content, where <![CDATA[ is meaningful. |
Constructor
html.Tokenizer(source: string, options: ?dict)
Parameters
source(string)options(?dict) —scripting(defaultfalse) andauto_content_state(defaulttrue).
Tokenizer.next_token()
html.Tokenizer.next_token() -> ?Token
The next token, or nil once the eof token has been returned.
The eof token is returned exactly once and is always the last one, so
a loop can stop on nil or on token.type == 'eof' interchangeably.
Returns ?Token
Tokenizer.set_state()
html.Tokenizer.set_state(name: string) -> Tokenizer
Puts the tokenizer into name, one of the state names listed on the
state field.
The tree builder calls this to enter RCDATA or RAWTEXT after a start tag, which is how the standard splits the work between the two stages.
Parameters
name(string)
Returns Tokenizer
Tokenizer.set_last_start_tag()
html.Tokenizer.set_last_start_tag(name: ?string) -> Tokenizer
Tells the tokenizer which start tag the tree builder most recently opened, so that RCDATA and RAWTEXT know which end tag closes them.
Parameters
name(?string)
Returns Tokenizer
2026, Richard Ore and The Zuri Contributors
wire
import wire
Wire, an HTML template engine.
A Wire template is an HTML document. Not a document with a templating
language sprinkled through it, and not a string that happens to end up
looking like HTML: the source is parsed by the same
WHATWG parser the html
module uses, and every valid HTML5 document is already a valid Wire
template. What Wire adds are attributes on ordinary elements and values
written between {{ and }}.
Parsing rather than substituting is what makes the rest possible. Wire
knows that one interpolation sits in a paragraph, another in an href,
another in a <script>, and it escapes each of them for where it
actually is. A value can never turn into structure.
import wire
echo wire.render_string(
'<p>Hello {{ name }}</p>',
{ name: '<b>Ada</b>' }
)
# <p>Hello <b>Ada</b></p>
Templates from files
render_string() is convenient for a one-off and for reading
documentation. Real templates live in files under a root directory,
which is templates beside the working directory unless you say
otherwise:
import wire
var view = wire.wire()
view.set_root('./views')
echo view.render('pages/home', { user })
The .html extension is added when the path as written names no file,
so 'pages/home' finds pages/home.html. set_extension() changes
that.
Every path resolves inside the root and nowhere else. One that
climbs out with .., or names an absolute path elsewhere, is refused.
That matters the moment a path is built from anything a request
supplied.
Values
Anything between {{ and }} is an expression. The full language is in
wire.expression; in practice it reads the way you would guess:
<h1>{{ post.title }}</h1>
<p>{{ post.author.name|upper }}</p>
<p>{{ post.tags|join(', ') }}</p>
<p>{{ post.views > 1000 ? 'popular' : 'quiet' }}</p>
<p>{{ user.nickname ?? user.name }}</p>
A name that was never supplied is nil, and so is a key read off it, so
{{ user.address.city }} on a request with no user renders as nothing
rather than failing. Ask with x-if when the difference matters.
Writing {{ literally is %{{.
Filters
A value passed through | goes through a filter. They chain, and they
take arguments:
{{ name|trim|title }}
{{ total|number_format(2) }}
{{ summary|truncate(80, '…') }}
wire.filters lists the forty-odd that ship with Wire, and
register_filter() adds your own.
Escaping, and why there is no way to turn it off
Every interpolation is escaped for the place it lands in:
| Where it is | What happens |
|---|---|
| text between tags | &, < and > become references |
| an attribute value | & and " become references |
href, src, action | the URL’s scheme is checked as well |
<script>, onclick | the value is encoded as JSON |
<style> | anything outside a CSS-safe set is dropped |
A javascript: URL in an href becomes about:blank.
set_url_schemes() changes the allowlist when an application really
does need another scheme.
Inside a <script> the value carries its own quotes, so do not add any:
<script>
var user = {{ name }}; // right, renders as "Ada"
var user = '{{ name }}'; // wrong, renders as '"Ada"'
</script>
Markup you built yourself and want rendered as markup goes through the
raw filter, or arrives already wrapped in wire.safe():
<div class="body">{{ article.rendered_html|raw }}</div>
raw is a promise that the value is safe where it lands. Applying it to
anything a user supplied is how a template engine becomes a cross-site
scripting hole.
Directives
Wire’s own attributes all begin with x-. Wire owns that prefix
outright: an x- attribute it does not recognize is an error rather
than something that quietly does nothing, so x-fi is caught the first
time the template compiles.
A directive can go on any element, and the element it goes on is part of
what it controls. Put it on <template> when you want the control
without an element in the output; <template> is the one element HTML
lets through untouched anywhere, including inside <head>, a table and
a <select>, and Wire removes it.
Choosing
<p x-if="user.is_admin">Admin</p>
<p x-elif="user.is_staff">Staff</p>
<p x-else>Member</p>
x-elif and x-else attach to the element sibling in front of them,
with only whitespace and comments allowed in between. x-not is the
inverse of x-if and takes no chain.
Repeating
<li x-for="items" x-value="item" x-key="index">
{{ index }}: {{ item.name }}
</li>
The element repeats, not just its children. Lists, dictionaries, strings
and ranges all iterate; a list binds x-key to the position and a
dictionary to the key.
Every iteration also publishes a loop variable:
| Field | Is |
|---|---|
loop.index | the position counting from 1 |
loop.index0 | the position counting from 0 |
loop.first | true on the first pass |
loop.last | true on the last |
loop.length | how many entries there are |
loop.even, .odd | whether the position is even or odd |
loop.key, .value | this entry, whether or not it is bound |
loop.parent | the enclosing loop’s own loop |
x-loop="row" renames it, which is how two nested loops can both be
read without going through parent.
An x-if on a repeated element is asked once per iteration, so it can
read the loop’s variables. It cannot be continued with x-elif there.
Content
<p x-text="summary"></p>
<div x-html="article.body|raw"></div>
<a x-attr="{ href: post.url, rel: post.external ? 'nofollow' : nil }">…</a>
x-text and x-html replace an element’s children. x-attr spreads a
dictionary onto it, where true gives a valueless attribute and false
or nil leaves the attribute off.
Composition
Including
<include path="partials/header.html" />
<template x-include="partials/header.html"></template>
Those two are the same thing. <include>, <extend>, <declare>,
<define> and <super> are sugar, rewritten into the directive form
while the source is being tokenized, which is why they work inside a
<head> or a table where an unknown element would be moved or dropped.
A path is read as text, not as an expression, so it can be written plainly and computed where it needs to be:
<include path="themes/{{ theme }}/header.html" />
An include sees the variables around it. x-with adds to them and
x-only withholds everything else, which is what turns a partial into a
component with a real interface:
<include path="components/badge" x-with="{ label: 'New', tone: 'green' }" only />
Anything written inside an include is handed to it as the region called
content, so a partial can wrap what it was given:
<!-- components/card.html -->
<div class="card">
<h3>{{ title }}</h3>
<declare name="content"></declare>
</div>
<!-- the page -->
<include path="components/card" x-with="{ title: 'Totals' }">
<p>{{ orders|length }} orders</p>
</include>
Inheriting
A base template marks the regions a page may replace:
<!-- layouts/page.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<declare name="head"></declare>
</head>
<body>
<main>
<declare name="content"></declare>
</main>
<footer>
<declare name="footer">
<p>© {{ year }}</p>
</declare>
</footer>
</body>
</html>
A page fills them in:
<!-- pages/home.html -->
<extend base="layouts/page.html">
<define name="content">
<h1>Hello {{ user.name }}</h1>
</define>
<define name="footer">
<super />
<p>All rights reserved.</p>
</define>
</extend>
A region nobody defines renders whatever the base put inside it. <super />
renders the definition this one replaces, so a page can add to a footer
instead of restating it. Inheritance nests as deep as you like, and a
middle template can declare regions of its own.
One template extends one base, and everything in an extending template
has to be inside a <define>, because the base is what decides the
structure. Defining the same region twice needs override said out
loud:
<define name="content" override>…</define>
Extending Wire from Zuri
import wire
var view = wire.wire()
# A filter: the value comes first, then whatever the template passed.
view.register_filter('excerpt', @(value, words) {
var count = words == nil ? 25 : words
return ' '.join(value.split('/\\s+/').take(count)) + '…'
})
# A value every template can see without being handed it.
view.register_global('site_name', 'Example')
# A function a template calls.
view.register_global('route', @(name) {
return '/' + name
})
Used as:
<p>{{ post.body|excerpt(40) }}</p>
<a href="{{ route('about') }}">{{ site_name }}</a>
register_element() claims a tag name outright and hands every one of
them to a function, which is the escape hatch for anything the
directives cannot express.
Compiling and caching
A template is parsed once, not once per render. render() compiles on
first use and keeps the result, checking the file’s modification time
and size before reusing it.
That check is one stat per template per render, which is what you want
while writing templates and not what you want under load.
set_auto_reload(false) turns it off, and clear_cache() is how a
long-running process picks up a deployment.
Comments
HTML comments are server-side notes and do not reach the page:
<!-- This never ships, and neither does {{ secret }}. -->
Nothing inside one is evaluated. set_comments(true) keeps them when a
comment is genuinely meant for the browser, as a conditional comment or
a build marker is.
When something is wrong
Everything Wire raises is a WireError and carries the template and the
line and column of the tag at fault. TemplateSyntaxError comes from
compiling, RenderError from rendering, and TemplateNotFoundError
from a path that goes nowhere.
catch {
echo view.render('pages/home', { user })
} as e {
if instance_of(e, wire.WireError) {
echo 'template ${e.location()}: ${e.reason}'
}
}
The module is callable. wire(...) is the same call as
wire.wire(...), documented below.
The wire API
Every public name in wire, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
wire.RenderError | class | Raised while rendering, for anything that depends on the values a template was given: iterating something… |
wire.Safe | class | A string that is already markup and must be written out as it is. |
wire.TemplateNotFoundError | class | Raised when a template file cannot be found, or when it resolves to somewhere outside the root directory. |
wire.TemplateSyntaxError | class | Raised while compiling a template, for anything Wire can tell is wrong without rendering: an unknown… |
wire.Wire | class | A template engine: a root directory, a set of filters, and a cache of everything compiled so far. |
wire.WireError | class | The base class of every error Wire raises. |
wire.compile.Attribute | class | One attribute of an element. |
wire.compile.Branch | class | One arm of a conditional chain. |
wire.compile.Comment | class | An HTML comment that survived into the output. |
wire.compile.Compiler | class | Compiles one template’s source. |
wire.compile.Conditional | class | A chain of x-if, x-elif and x-else, or a lone x-if. |
wire.compile.Element | class | An element, with everything about it already worked out. |
wire.compile.Group | class | A run of instructions with nothing of its own, which is what a <template> carrying a directive leaves… |
wire.compile.Include | class | Another template rendered in this instruction’s place. |
wire.compile.Instruction | class | The base of every instruction, carrying the kind the renderer switches on and the place in the source it came… |
wire.compile.Loop | class | A repetition, from x-for. |
wire.compile.Raw | class | Characters written out exactly as they are, used for the doctype. |
wire.compile.Segment | class | One piece of a run of text or of an attribute value: either literal characters or an expression to evaluate. |
wire.compile.Slot | class | A region an extending template may replace, from x-slot. |
wire.compile.Super | class | The definition this one replaces, from x-super. |
wire.compile.Template | class | A compiled template, ready to render as many times as you like. |
wire.compile.Text | class | A run of text, possibly with interpolations in it. |
wire.compile.compile | function | Compiles source into a Template. |
wire.constants.ATTR_ATTR | constant | Spreads a dictionary of name/value pairs onto the element as attributes. |
wire.constants.CALL_CLOSE | constant | Closes a template function call. |
wire.constants.CALL_OPEN | constant | Opens a template function call. |
wire.constants.CARRIER_TAG | constant | The element Wire uses to carry a directive without emitting anything of its own. |
wire.constants.DEFAULT_EXTENSION | constant | The extension render() appends when the path it was given does not name an existing file and carries no… |
wire.constants.DEFAULT_LOOP_NAME | constant | The variable an x-for publishes its loop metadata under. |
wire.constants.DEFAULT_ROOT | constant | The directory render() resolves template paths against until set_root() says otherwise, which is a… |
wire.constants.DEFINE_ATTR | constant | Replaces the base template’s region of the same name. |
wire.constants.DIRECTIVES | constant | Every directive attribute Wire understands. |
wire.constants.DIRECTIVE_PREFIX | constant | The prefix every Wire directive attribute carries. |
wire.constants.ELIF_ATTR | constant | Continues an x-if chain on the next element sibling. |
wire.constants.ELSE_ATTR | constant | Closes an x-if chain on the next element sibling. |
wire.constants.ESCAPE_CHAR | constant | The character that escapes an interpolation, making Wire emit the braces literally instead of reading what is… |
wire.constants.EXPRESSION_DIRECTIVES | constant | The directives whose value is a Wire expression. |
wire.constants.EXTEND_ATTR | constant | Marks this template as extending the base template named by the value. |
wire.constants.FLAG_DIRECTIVES | constant | The directives that take no value at all. |
wire.constants.FOR_ATTR | constant | Repeats the element once per entry of the expression’s value. |
wire.constants.HTML_ATTR | constant | Replaces the element’s children with the expression’s value as markup, without escaping it. |
wire.constants.IF_ATTR | constant | Renders the element only when the expression is truthy. |
wire.constants.INCLUDE_ATTR | constant | Renders another template in this element’s place. |
wire.constants.KEY_ATTR | constant | Names the variable each x-for iteration binds its key or index to. |
wire.constants.LOOP_ATTR | constant | Renames the loop metadata variable an x-for publishes, which is called loop unless this says otherwise. |
wire.constants.MAX_DEPTH | constant | How deep x-include and x-extend may nest before Wire calls it a cycle. |
wire.constants.NAME_DIRECTIVES | constant | The directives whose value names a variable or a region, and is read as a bare identifier rather than… |
wire.constants.NOT_ATTR | constant | Renders the element only when the expression is falsy, the inverse of x-if. |
wire.constants.ONLY_ATTR | constant | Withholds the surrounding scope from an x-include or a component, leaving it only what x-with passes. |
wire.constants.OVERRIDE_ATTR | constant | Permits an x-define to replace a definition of the same name made earlier in the same template. |
wire.constants.PATH_DIRECTIVES | constant | The directives whose value is a template path, read as literal text with {{ }} interpolation allowed inside… |
wire.constants.PSEUDO_ELEMENTS | constant | The pseudo elements Wire accepts as sugar, each mapped to the directive it becomes and the attribute that… |
wire.constants.PSEUDO_FLAGS | constant | Attributes a pseudo element may carry that become valueless directives rather than the directive its name… |
wire.constants.SLOT_ATTR | constant | Declares a region an extending template may replace. |
wire.constants.SUPER_ATTR | constant | Renders the definition this one replaces. |
wire.constants.TEXT_ATTR | constant | Replaces the element’s children with the expression’s value as text. |
wire.constants.VALUE_ATTR | constant | Names the variable each x-for iteration binds its value to. |
wire.constants.VAR_CLOSE | constant | Closes an interpolation. |
wire.constants.VAR_OPEN | constant | Opens an interpolation. |
wire.constants.WITH_ATTR | constant | Supplies the variables an x-include or a component is rendered with. |
wire.escape.BLOCKED_URL | constant | What a blocked URL is replaced with. |
wire.escape.CONTEXT_ATTRIBUTE | constant | An ordinary attribute value. |
wire.escape.CONTEXT_SCRIPT | constant | A place the browser reads as JavaScript: the body of a <script>, or the value of an on* handler attribute. |
wire.escape.CONTEXT_STYLE | constant | The body of a <style> element. |
wire.escape.CONTEXT_TEXT | constant | Text between tags. |
wire.escape.CONTEXT_URL | constant | An attribute the browser resolves as a URL. |
wire.escape.DEFAULT_URL_SCHEMES | constant | The URL schemes Wire lets through by default. |
wire.escape.STYLE_SAFE | constant | The characters a value may contain inside a <style> body. |
wire.escape.URL_ATTRIBUTES | constant | The attributes whose value a browser resolves as a URL, and which therefore have to be checked for a… |
wire.escape.URL_LIST_ATTRIBUTES | constant | URL attributes holding a list of URLs rather than a single one. |
wire.escape.attribute | function | Escapes value for a double quoted attribute value, turning & and " into character references. |
wire.escape.attribute_context | function | The escaping context an attribute named name calls for. |
wire.escape.escape | function | Escapes value for the given context. |
wire.escape.is_script_attribute | function | Whether name is an event handler attribute, whose value a browser reads as JavaScript. |
wire.escape.is_url_attribute | function | Whether name is an attribute the browser resolves as a single URL. |
wire.escape.is_url_list_attribute | function | Whether name is an attribute holding a list of URLs. |
wire.escape.script | function | Escapes value for a place the browser reads as JavaScript, by encoding it as JSON and then hiding the… |
wire.escape.style | function | Escapes value for the body of a <style> element by dropping every character outside a conservative… |
wire.escape.text | function | Escapes value for text between tags, turning &, < and > into character references. |
wire.escape.text_context | function | The escaping context text inside an element named tag calls for. |
wire.escape.url | function | Escapes value for an attribute the browser resolves as a URL, replacing it with about:blank when its… |
wire.escape.url_list | function | Escapes value for an attribute holding several URLs, checking each entry’s scheme on its own. |
wire.escape.uses_descriptors | function | Whether a URL list attribute allows a descriptor after each URL, which srcset does and ping does not. |
wire.expression.Binary | class | An arithmetic or comparison operator. |
wire.expression.Call | class | A call, as in route('home') or user.display_name(). |
wire.expression.Conditional | class | condition ? consequence : alternative. |
wire.expression.DictLiteral | class | A dictionary literal. |
wire.expression.Expression | class | The base of every node in a parsed expression. |
wire.expression.Filter | class | A value passed through a filter, as in name|upper. |
wire.expression.Index | class | A bracketed lookup, as in items[index], where the key is itself an expression. |
wire.expression.KEYWORDS | constant | The words that are part of the language rather than names a template can bind. |
wire.expression.LONG_OPERATORS | constant | Operators made of more than one character, longest first so that <= is never read as < followed by =. |
wire.expression.ListLiteral | class | A list literal. |
wire.expression.Literal | class | A number, a string, or one of true, false and nil. |
wire.expression.Logical | class | and, or or ??, each of which decides whether to evaluate its right side after looking at its left. |
wire.expression.Member | class | A dotted lookup, as in user.name. |
wire.expression.Parser | class | Builds a syntax tree from an expression’s tokens. |
wire.expression.SHORT_OPERATORS | constant | Operators made of a single character. |
wire.expression.Token | class | One piece of an expression’s source: its kind, its value, and where in the expression it started. |
wire.expression.Unary | class | not x, !x or -x. |
wire.expression.Variable | class | A bare name, looked up in the variables the template was rendered with. |
wire.expression.parse | function | Compiles source into a syntax tree. |
wire.expression.tokenize | function | Splits source into tokens. |
wire.filters.BUILTIN | constant | Every filter Wire starts with, keyed by the name a template calls it by. |
wire.filters.abs | function | The value without its sign. |
wire.filters.capitalize | function | The value with its first letter in upper case and the rest left alone. |
wire.filters.ceil | function | The smallest whole number at or above the value. |
wire.filters.date | function | A date written out with the given format. |
wire.filters.default_to | function | fallback when the value is falsy, otherwise the value. |
wire.filters.empty | function | Whether the value has nothing in it. |
wire.filters.escape_value | function | Escapes a value for a context other than the one it is being written into, or escapes a value that was… |
wire.filters.filesize | function | A byte count written the way a person reads it. |
wire.filters.first | function | The first entry, or nil when there is none. |
wire.filters.floor | function | The largest whole number at or below the value. |
wire.filters.is | function | Whether the value equals expected. |
wire.filters.join | function | The entries joined into one string with glue between them. |
wire.filters.json | function | The value as JSON. |
wire.filters.json_script | function | The value as JSON wrapped in a <script type="application/json"> element, ready to be read back by a script… |
wire.filters.keys | function | A dictionary’s keys, in insertion order. |
wire.filters.last | function | The last entry, or nil when there is none. |
wire.filters.length | function | How many entries the value has. |
wire.filters.lower | function | The value in lower case. |
wire.filters.lpad | function | The value padded on the left with fill until it is width characters long. |
wire.filters.nl2br | function | The value with its line breaks turned into elements. |
wire.filters.not | function | Whether the value differs from expected. |
wire.filters.number_format | function | The value written out with thousands separated and a fixed number of decimal places. |
wire.filters.raw | function | Marks a value as markup so Wire writes it out without escaping it. |
wire.filters.repeat | function | The value repeated count times. |
wire.filters.replace | function | The value with every occurrence of search replaced by replacement. |
wire.filters.reverse | function | The entries in the opposite order, or a string backwards. |
wire.filters.round | function | The value rounded to places decimal places. |
wire.filters.rpad | function | The value padded on the right with fill until it is width characters long. |
wire.filters.slice | function | The entries from start up to but not including end. |
wire.filters.slug | function | The value as a lowercase, hyphen separated slug. |
wire.filters.sort | function | The entries in ascending order. |
wire.filters.split | function | The value split into a list on separator. |
wire.filters.strip_tags | function | The value with every HTML tag removed, leaving only its text. |
wire.filters.sum | function | The entries added together. |
wire.filters.title | function | The value with the first letter of every word in upper case and the rest in lower case. |
wire.filters.trim | function | The value with leading and trailing whitespace removed. |
wire.filters.truncate | function | The value cut down to length characters, with suffix put on the end when anything was actually cut. |
wire.filters.unique | function | The entries with later duplicates removed, keeping the first of each. |
wire.filters.upper | function | The value in upper case. |
wire.filters.url_encode | function | The value percent encoded for use inside a URL. |
wire.filters.values | function | A dictionary’s values, in insertion order. |
wire.is_safe | function | True when value is a Safe. |
wire.loader.Loader | class | Finds template files under one root directory. |
wire.normalize.Normalizer | class | A tokenizer that rewrites Wire’s pseudo elements on the way past. |
wire.normalize.parse | function | Parses source into a tree with Wire’s pseudo elements already rewritten. |
wire.render | function | Renders the template at path using the shared Wire. |
wire.render.Definition | class | One definition of a region, and what to render it against. |
wire.render.Frame | class | One level of variables, pointing at the level around it. |
wire.render.Renderer | class | Renders compiled templates. |
wire.render_string | function | Renders source using the shared Wire. |
wire.safe | function | Marks value as markup that Wire must not escape. |
wire.shared | function | The Wire behind the module-level render() and render_string(). |
wire.stringify | function | What value looks like once it reaches the page, before escaping. |
wire.truthy | function | Whether value counts as true in an x-if, an x-not, or a boolean operator inside an expression. |
wire.values.MAX_ARGUMENTS | constant | The most arguments a filter or a template function can be called with. |
wire.values.compare | function | Orders a before, with or after b, returning -1, 0 or 1. |
wire.values.invoke | function | Calls target with arguments spread into its parameters. |
wire.values.is_blank | function | Whether text is empty or is nothing but whitespace. |
wire.values.is_empty | function | Whether value has nothing in it, for the empty filter and for anything else that wants the question asked… |
wire.values.type_name | function | Wire’s name for value’s type, used in error messages so that a complaint reads “expected a list, got a… |
wire.values.unwrap | function | Strips the Safe wrapper off value, leaving anything else alone. |
wire.wire | function | A new Wire. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
wire.compile | import wire.compile | Turning a parsed template into the instruction tree the renderer walks. |
wire.constants | wire.constants.* | The names Wire reserves: its directive attributes, the pseudo elements that are sugar for them, and the… |
wire.errors | wire.errors.* | The errors Wire raises, and the location information they carry. |
wire.escape | wire.escape.* | Turning a value into something safe to write at a particular place in a page. |
wire.expression | wire.expression.* | Wire’s expression language: the thing that sits between {{ and }}, and the thing an x-if or an x-for… |
wire.filters | wire.filters.* | The filters every Wire template starts with. |
wire.loader | import wire.loader | Turning the path written in an x-include into a file on disk, and refusing to when it points somewhere it… |
wire.normalize | wire.normalize.* | Rewriting Wire’s pseudo elements into something HTML’s own tree construction will not move, drop or reshape. |
wire.render | import wire.render | Walking a compiled template and writing the page out. |
wire.values | wire.values.* | How Wire reads the values a template is given: what counts as true, what a value looks like once it reaches… |
Functions
wire()
wire.wire(options: ?dict) -> Wire
A new Wire.
options takes any of root, extension, compact, comments,
auto_reload and url_schemes, each the same as the matching set_
method, so a whole configuration can be written in one place.
import wire
var view = wire.wire({
root: './views',
auto_reload: false,
})
Parameters
options(?dict)
Returns Wire
shared()
wire.shared() -> Wire
The Wire behind the module-level render() and render_string().
Configure this to use those without building your own, which is worth doing for a small program and not worth doing for anything that wants two different roots.
import wire
wire.shared().set_root('./views')
echo wire.render('pages/home', { user })
Returns Wire
render()
wire.render(path: string, variables: ?dict) -> string
Renders the template at path using the shared Wire.
Parameters
path(string)variables(?dict)
Returns string
Raises WireError
render_string()
wire.render_string(source: string, variables: ?dict, path: ?string) -> string
Renders source using the shared Wire.
Parameters
source(string)variables(?dict)path(?string)
Returns string
Raises WireError
Classes
Wire
class wire.Wire
A template engine: a root directory, a set of filters, and a cache of everything compiled so far.
One of these is normally built at startup, configured once, and used for the life of the process. Rendering does not change it, so it is safe to render from several places at once.
import wire
var view = wire.wire()
view.set_root('./views')
view.register_global('site_name', 'Example')
echo view.render('pages/home', { user })
Fields
| Field | Type | Description |
|---|---|---|
globals | Values every template can read without being handed them. | |
url_schemes | The URL schemes allowed in an href and its relatives. |
Constructor
wire.Wire(options: ?dict)
Parameters
options(?dict) — Any ofroot,extension,compact,comments,auto_reloadandurl_schemes, each the same as the matchingset_method.
Raises ArgumentError when an option is not of the type it should
be.
Wire.set_root()
wire.Wire.set_root(path: string) -> Wire
Sets the directory template paths resolve inside.
A relative path is taken from the working directory. The directory does not have to exist yet; a path under one that does not is reported as not found like any other.
Changing the root empties the cache, since the same path can now mean a different file.
Parameters
path(string)
Returns Wire
Raises ArgumentError
Wire.root()
wire.Wire.root() -> string
The directory template paths resolve inside, as an absolute path.
Returns string
Wire.create_root()
wire.Wire.create_root() -> bool
Creates the root directory when it does not exist, and reports whether it had to.
Wire never does this on its own. Creating directories is not a template engine’s business, and a typo in a root should read as a missing template rather than quietly produce an empty directory.
Returns bool
Raises Error when the directory cannot be created.
Wire.set_extension()
wire.Wire.set_extension(extension: string) -> Wire
Sets the extension tried when a path names no file as written.
It has to begin with a dot. render('home') then looks for home and
then for home plus this.
Parameters
extension(string)
Returns Wire
Raises ArgumentError when it does not begin with a dot.
Wire.set_compact()
wire.Wire.set_compact(compact: bool) -> Wire
Drops whitespace-only text from the output when compact is true.
This is the cheap kind of minification: the indentation between tags
goes, and nothing else is touched. Whitespace inside a <pre> is
whitespace-only only when the whole run is, so this can still change how
preformatted text reads; leave it off where that matters and reach for
html.minify() instead, which knows the difference.
Parameters
compact(bool)
Returns Wire
Raises ArgumentError
Wire.set_comments()
wire.Wire.set_comments(comments: bool) -> Wire
Keeps HTML comments in the output when comments is true.
Comments are dropped by default, which is what makes an HTML comment a server-side note. Nothing inside one is ever evaluated either way.
Parameters
comments(bool)
Returns Wire
Raises ArgumentError
Wire.set_auto_reload()
wire.Wire.set_auto_reload(reload: bool) -> Wire
Checks a cached template against its file before reusing it when reload is true, which is the default.
The check is one stat per template per render. Leave it on while
templates are being written and turn it off where throughput matters,
remembering that clear_cache() is then the only way a running process
notices a deployment.
Parameters
reload(bool)
Returns Wire
Raises ArgumentError
Wire.set_url_schemes()
wire.Wire.set_url_schemes(schemes: list) -> Wire
Replaces the URL schemes allowed in an href and its relatives.
A URL with no scheme is always allowed, so every relative link keeps
working whatever this is set to. A URL whose scheme is not listed is
replaced with about:blank.
The default is in escape.DEFAULT_URL_SCHEMES and deliberately leaves
out javascript, vbscript, data and file.
view.set_url_schemes(escape.DEFAULT_URL_SCHEMES + ['app'])
Parameters
schemes(list) — Scheme names without the colon, in lower case.
Returns Wire
Raises ArgumentError
Wire.register_filter()
wire.Wire.register_filter(name: string, handler) -> Wire
Registers a filter templates can use after a |.
The value being filtered arrives as the first argument and whatever the
template passed follows it. An argument the template left out arrives as
nil, so give it a default rather than insisting.
Registering a name that already exists replaces it, which is how an
application makes date mean its own thing.
view.register_filter('excerpt', @(value, words) {
var count = words == nil ? 25 : words
return ' '.join(value.split('/\\s+/').take(count)) + '…'
})
A filter returning a plain string has its result escaped like any other
value. One that genuinely produces markup returns wire.safe(), and is
then responsible for what is inside it.
Parameters
name(string)handler(callable)
Returns Wire
Raises ArgumentError
Wire.register_element()
wire.Wire.register_element(name: string, handler) -> Wire
Claims a tag name and hands every element of that name to handler instead of writing it out.
The handler is called with the Wire and a dictionary describing the
element:
| Key | Is |
|---|---|
name | the tag name |
attributes | its attributes, with every value already rendered and escaped |
content | its children, already rendered |
variables | the scope it was written in |
Return wire.safe(markup) to write markup, any other value to write it
as escaped text, or nil to write nothing at all.
view.register_element('icon', @(view, element) {
var name = element.attributes.get('name', 'dot')
return wire.safe('<svg class="icon"><use href="#${name}"></use></svg>')
})
Directives still work on a claimed element, so <icon x-if="…" />
behaves the way it reads. Reach for this only where the directives
genuinely cannot express something; a partial and x-include is easier
to read and does not need Zuri code to follow it.
Parameters
name(string)handler(callable)
Returns Wire
Raises ArgumentError
Wire.register_global()
wire.Wire.register_global(name: string, value) -> Wire
Makes value readable from every template by name.
A variable of the same name passed to render() wins, so a global is a
default rather than an override. A callable global is a function a
template can call.
Parameters
name(string)value(any)
Returns Wire
Raises ArgumentError
Wire.render()
wire.Wire.render(path: string, variables: ?dict) -> string
Renders the template at path and returns the page.
path is relative to the root and may leave off the extension. variables is what the template can read, and defaults to nothing at all.
echo view.render('pages/home', { user, posts })
Parameters
path(string)variables(?dict)
Returns string
Raises TemplateNotFoundError when the template does not exist or
is outside the root.
Raises TemplateSyntaxError when it does not compile.
Raises RenderError when rendering it fails.
Wire.render_string()
wire.Wire.render_string(source: string, variables: ?dict, path: ?string) -> string
Renders source as a template and returns the page.
path names the source in any error raised and defaults to <source>.
Includes and extends inside the source still resolve against the root.
The result is not cached, since there is no file to key it on. Use
render() for anything rendered more than once.
echo view.render_string('<p>{{ greeting }}</p>', { greeting: 'Hi' })
Parameters
source(string)variables(?dict)path(?string)
Returns string
Raises TemplateSyntaxError
Raises RenderError
Wire.compile()
wire.Wire.compile(path: string) -> Template
Compiles the template at path without rendering it, and returns it.
Useful for checking a directory of templates at build or startup time, so that a syntax error is found then rather than on the request that reaches it.
for name in os.list_dir('./views') {
view.compile(name)
}
Parameters
path(string)
Returns Template
Raises TemplateNotFoundError
Raises TemplateSyntaxError
Wire.clear_cache()
wire.Wire.clear_cache() -> Wire
Forgets every compiled template.
A long-running process with set_auto_reload(false) needs this to
notice a deployment.
Returns Wire
Wire.load()
wire.Wire.load(path: string, context, from: ?string, line: ?number, column: ?number) -> Template
The compiled template at path, from the cache when it is there and still current.
context names the element the template will sit inside, so that a partial holding table rows is parsed knowing it is going into a table. from, line and column describe where the include was written, for the error when the path goes nowhere.
The renderer calls this; there is rarely a reason to call it directly.
Parameters
path(string)context(?string)from(?string)line(?number)column(?number)
Returns Template
Raises TemplateNotFoundError
Raises TemplateSyntaxError
Wire.filter()
wire.Wire.filter(name: string) -> ?callable
The filter called name, or nil.
Parameters
name(string)
Returns ?callable
Wire.element()
wire.Wire.element(name: string) -> ?callable
The handler registered for the tag name, or nil.
Parameters
name(string)
Returns ?callable
2026, Richard Ore and The Zuri Contributors
wire.compile
import wire.compile
wiredoes not re-export this module, so it is reached only by importing it directly.
Turning a parsed template into the instruction tree the renderer walks.
Wire compiles rather than substitutes. A template is parsed once, every expression in it is parsed once, every directive is resolved once, and what comes out is a tree of plain instructions that holds no reference back to the document it came from. Rendering that tree is then a walk with no parsing in it at all, which is what lets the same page be rendered thousands of times from one compile, and what lets the renderer know the exact escaping context of every interpolation instead of guessing at it.
Compiling is also where a template’s mistakes are found. An unknown
directive, an expression that does not parse, an x-else with no x-if
in front of it, the same region defined twice: all of them are reported
here, once, rather than on the request that happens to reach them.
Functions
compile()
wire.compile.compile(source: string, path: string, context, options: ?dict) -> Template
Compiles source into a Template.
context names the element the result will sit inside, and should be
nil for a template rendered on its own. options takes compact
(drop whitespace-only text, default false) and comments (keep HTML
comments, default false).
import wire
var template = wire.compile.compile(
'<p x-if="user">Hello {{ user.name }}</p>', 'greeting.html', nil, nil
)
Parameters
source(string)path(string)context(?string)options(?dict)
Returns Template
Raises TemplateSyntaxError
Classes
Segment
class wire.compile.Segment
One piece of a run of text or of an attribute value: either literal characters or an expression to evaluate.
Fields
| Field | Type | Description |
|---|---|---|
text | The literal characters, or nil when this is an expression. | |
value | The expression, or nil when this is literal text. |
Constructor
wire.compile.Segment(text, value)
Parameters
text(?string)value(?Expression)
Instruction
class wire.compile.Instruction
The base of every instruction, carrying the kind the renderer switches on and the place in the source it came from.
Fields
| Field | Type | Description |
|---|---|---|
kind | What kind of instruction this is. | |
line | The line of the tag it came from, or 0. | |
column | The column of that tag, or 0. |
Constructor
wire.compile.Instruction(kind: string, line: number, column: number)
Parameters
kind(string)line(number)column(number)
Text
class wire.compile.Text < Instruction
A run of text, possibly with interpolations in it.
Fields
| Field | Type | Description |
|---|---|---|
segments | The literal and interpolated pieces, in order. | |
context | The escaping context every interpolation in this run needs. | |
raw_literal | True when the literal pieces are written out unchanged, which is the case inside a <script> or a <style>… |
Constructor
wire.compile.Text(segments: list, context: string, raw_literal: bool)
Parameters
segments(list)context(string)raw_literal(bool)
Raw
class wire.compile.Raw < Instruction
Characters written out exactly as they are, used for the doctype.
Fields
| Field | Type | Description |
|---|---|---|
text | The characters. |
Constructor
wire.compile.Raw(text: string)
Parameters
text(string)
Comment
class wire.compile.Comment < Instruction
An HTML comment that survived into the output.
Fields
| Field | Type | Description |
|---|---|---|
data | The text between the delimiters. |
Constructor
wire.compile.Comment(data: string)
Parameters
data(string)
Attribute
class wire.compile.Attribute
One attribute of an element.
Fields
| Field | Type | Description |
|---|---|---|
name | The attribute’s name. | |
segments | The literal and interpolated pieces of its value. | |
context | The escaping context its interpolations need, decided by the attribute’s name. | |
url_list | True when the attribute holds several URLs rather than one, so each has to be checked on its own. | |
url_descriptors | True when each URL in the list may be followed by a descriptor, as srcset allows and ping does not. |
Constructor
wire.compile.Attribute(name: string, segments: list)
Parameters
name(string)segments(list)
Element
class wire.compile.Element < Instruction
An element, with everything about it already worked out.
Fields
| Field | Type | Description |
|---|---|---|
tag | The tag name, as it will be written. | |
attributes | The attributes written literally on the element. | |
spreads | Expressions giving further attributes, from x-attr, applied after the literal ones so a computed value wins. | |
children | The instructions for the element’s children. | |
void | True for an element written without a closing tag. | |
content | An expression replacing the element’s children, from x-text or x-html, or nil. | |
content_raw | True when content came from x-html and must not be escaped. |
Constructor
wire.compile.Element(tag: string, line: number, column: number)
Parameters
tag(string)line(number)column(number)
Group
class wire.compile.Group < Instruction
A run of instructions with nothing of its own, which is what a
<template> carrying a directive leaves behind.
Fields
| Field | Type | Description |
|---|---|---|
children | The instructions. |
Constructor
wire.compile.Group(children: list)
Parameters
children(list)
Branch
class wire.compile.Branch
One arm of a conditional chain.
Fields
| Field | Type | Description |
|---|---|---|
test | The expression being tested, or nil for the final x-else. | |
negate | True when the arm renders on a falsy test, which is what x-not means. | |
body | The instruction to render when the arm is taken. |
Constructor
wire.compile.Branch(test, negate: bool, body)
Parameters
test(?Expression)negate(bool)body(Instruction)
Conditional
class wire.compile.Conditional < Instruction
A chain of x-if, x-elif and x-else, or a lone x-if.
Fields
| Field | Type | Description |
|---|---|---|
branches | The arms, in the order they were written. |
Constructor
wire.compile.Conditional(branches: list, line: number, column: number)
Parameters
branches(list)line(number)column(number)
Loop
class wire.compile.Loop < Instruction
A repetition, from x-for.
Fields
| Field | Type | Description |
|---|---|---|
sequence | The expression giving the thing to iterate. | |
value_name | The name each value is bound to, or nil. | |
key_name | The name each key or index is bound to, or nil. | |
loop_name | The name the loop’s own metadata is published under. | |
body | The instruction rendered once per entry. |
Constructor
wire.compile.Loop(sequence, line: number, column: number)
Parameters
sequence(Expression)line(number)column(number)
Include
class wire.compile.Include < Instruction
Another template rendered in this instruction’s place.
Fields
| Field | Type | Description |
|---|---|---|
path | The literal and interpolated pieces of the path. | |
context | The element the include sits inside, so a partial holding table rows is parsed knowing that, or nil for a… | |
variables | An expression giving further variables for the included template, from x-with, or nil. | |
only | True when the included template sees only what x-with gave it. | |
body | The instructions for the include’s own children, which the included template can place with… |
Constructor
wire.compile.Include(path: list, context, line: number, column: number)
Parameters
path(list)context(?string)line(number)column(number)
Slot
class wire.compile.Slot < Instruction
A region an extending template may replace, from x-slot.
Fields
| Field | Type | Description |
|---|---|---|
name | The region’s name. | |
body | The instructions rendered when nothing has replaced it. |
Constructor
wire.compile.Slot(name: string, body: list, line: number, column: number)
Parameters
name(string)body(list)line(number)column(number)
Super
class wire.compile.Super < Instruction
The definition this one replaces, from x-super.
Constructor
wire.compile.Super(line: number, column: number)
Parameters
line(number)column(number)
Template
class wire.compile.Template
A compiled template, ready to render as many times as you like.
Nothing here changes while a render is running, so one of these can be shared.
Fields
| Field | Type | Description |
|---|---|---|
path | The file it was compiled from, or the source name given for a template compiled from a string. | |
body | The instructions making up the template’s own output. | |
blocks | The regions this template defines, keyed by name, from x-define. | |
extends | The literal and interpolated pieces of the base template’s path, or nil when this template extends nothing. | |
context | The element this template was compiled to sit inside, or nil when it was compiled on its own. | |
fingerprint | What the file looked like when it was read, so a cached template can tell whether the file has changed… | |
errors | The parse errors the HTML parser found in the source. |
Constructor
wire.compile.Template(path: string, context)
Parameters
path(string)context(?string)
Compiler
class wire.compile.Compiler
Compiles one template’s source.
There is one of these per compile and it is thrown away afterwards; the
Template it produces holds nothing that points back at it.
Fields
| Field | Type | Description |
|---|---|---|
path | The template being compiled, for error messages. | |
compact | Whether whitespace-only text is dropped. | |
comments | Whether comments survive into the output. |
Constructor
wire.compile.Compiler(path: string, options: ?dict)
Parameters
path(string)options(?dict)
Compiler.compile()
wire.compile.Compiler.compile(source: string, context) -> Template
Compiles source as a template that will sit inside context, or on
its own when context is nil.
Parameters
source(string)context(?string)
Returns Template
Raises TemplateSyntaxError
Compiler.children()
wire.compile.Compiler.children(nodes: list) -> list
Compiles a list of sibling nodes, tying conditional chains together as it goes.
Parameters
nodes(list)
Returns list
Raises TemplateSyntaxError
Compiler.other()
wire.compile.Compiler.other(node) -> ?Instruction
Compiles a node that is not an element, or returns nil when it
contributes nothing.
Parameters
node(Node)
Returns ?Instruction
Compiler.text()
wire.compile.Compiler.text(node) -> Text
Compiles a text node, working out its escaping context from the element it sits in.
Parameters
node(Text)
Returns Text
Compiler.element()
wire.compile.Compiler.element(node) -> Instruction
Compiles an element, wrapping it in whatever its directives ask for.
The wrapping runs outside in: a repetition holds a condition, a
condition holds an include, and the element itself is innermost. That
order is what lets an x-if on a repeated element see the loop’s own
variables.
Parameters
node(Element)
Returns Instruction
Raises TemplateSyntaxError
Compiler.expression_of()
wire.compile.Compiler.expression_of(node, directive: string) -> Expression
Parses the expression a directive carries.
Parameters
node(Element)directive(string)
Returns Expression
Raises TemplateSyntaxError
Compiler.name_of()
wire.compile.Compiler.name_of(node, directive: string) -> string
Reads the bare identifier a naming directive carries.
Parameters
node(Element)directive(string)
Returns string
Raises TemplateSyntaxError when it is not a plain name.
Compiler.path_segments()
wire.compile.Compiler.path_segments(value: string, node) -> list
Splits a template path into its literal and interpolated pieces.
A path is read as text rather than as an expression, so
x-include="header.html" names a file instead of reading a html key
off a header variable. Use {{ }} where part of it is computed.
Parameters
value(string)node(Element)
Returns list
Raises TemplateSyntaxError
Compiler.segments()
wire.compile.Compiler.segments(value: string, context: string, host) -> list
Splits text into its literal and interpolated pieces.
Parameters
value(string)context(string)host(?Node) — The node the text belongs to, for error positions.
Returns list
Raises TemplateSyntaxError
2026, Richard Ore and The Zuri Contributors
wire.constants
import wire
wireexposes this aswire.constants, soimport wireis enough and the names are called aswire.constants.*.import wire.constantsreaches the same definitions directly.
The names Wire reserves: its directive attributes, the pseudo elements
that are sugar for them, and the handful of defaults the Wire class
starts out with.
Everything a template author can type that Wire treats specially is named here rather than spelled inline, so the answer to “is this attribute mine?” only ever has one place to live.
Constants
DIRECTIVE_PREFIX
wire.constants.DIRECTIVE_PREFIX = 'x-'
The prefix every Wire directive attribute carries.
Wire owns this prefix outright: an attribute starting with x- that
Wire does not recognize is a compile error rather than a passthrough,
which is what turns a typo like x-fi into a message instead of a
silently missing section. Markup that genuinely needs an x- prefixed
attribute in the output can emit one through x-attr.
CARRIER_TAG
wire.constants.CARRIER_TAG = 'template'
The element Wire uses to carry a directive without emitting anything of its own.
<template> is the only HTML element the tree construction algorithm
lets through untouched everywhere: inside <head>, inside a table,
inside a <select>. Every other name would be relocated or dropped by
the parser before Wire ever saw it, which is why Wire’s own pseudo
elements are rewritten into this one while the source is being
tokenized.
IF_ATTR
wire.constants.IF_ATTR = 'x-if'
Renders the element only when the expression is truthy.
ELIF_ATTR
wire.constants.ELIF_ATTR = 'x-elif'
Continues an x-if chain on the next element sibling.
ELSE_ATTR
wire.constants.ELSE_ATTR = 'x-else'
Closes an x-if chain on the next element sibling. Valueless.
NOT_ATTR
wire.constants.NOT_ATTR = 'x-not'
Renders the element only when the expression is falsy, the inverse of
x-if.
FOR_ATTR
wire.constants.FOR_ATTR = 'x-for'
Repeats the element once per entry of the expression’s value.
VALUE_ATTR
wire.constants.VALUE_ATTR = 'x-value'
Names the variable each x-for iteration binds its value to.
KEY_ATTR
wire.constants.KEY_ATTR = 'x-key'
Names the variable each x-for iteration binds its key or index to.
LOOP_ATTR
wire.constants.LOOP_ATTR = 'x-loop'
Renames the loop metadata variable an x-for publishes, which is called
loop unless this says otherwise.
TEXT_ATTR
wire.constants.TEXT_ATTR = 'x-text'
Replaces the element’s children with the expression’s value as text.
HTML_ATTR
wire.constants.HTML_ATTR = 'x-html'
Replaces the element’s children with the expression’s value as markup, without escaping it.
ATTR_ATTR
wire.constants.ATTR_ATTR = 'x-attr'
Spreads a dictionary of name/value pairs onto the element as attributes.
INCLUDE_ATTR
wire.constants.INCLUDE_ATTR = 'x-include'
Renders another template in this element’s place.
WITH_ATTR
wire.constants.WITH_ATTR = 'x-with'
Supplies the variables an x-include or a component is rendered with.
ONLY_ATTR
wire.constants.ONLY_ATTR = 'x-only'
Withholds the surrounding scope from an x-include or a component,
leaving it only what x-with passes. Valueless.
EXTEND_ATTR
wire.constants.EXTEND_ATTR = 'x-extend'
Marks this template as extending the base template named by the value.
SLOT_ATTR
wire.constants.SLOT_ATTR = 'x-slot'
Declares a region an extending template may replace.
DEFINE_ATTR
wire.constants.DEFINE_ATTR = 'x-define'
Replaces the base template’s region of the same name.
OVERRIDE_ATTR
wire.constants.OVERRIDE_ATTR = 'x-override'
Permits an x-define to replace a definition of the same name made
earlier in the same template. Valueless.
SUPER_ATTR
wire.constants.SUPER_ATTR = 'x-super'
Renders the definition this one replaces. Valueless.
DIRECTIVES
wire.constants.DIRECTIVES = [...]
Every directive attribute Wire understands.
A template carrying an x- prefixed attribute outside this list is
rejected at compile time.
EXPRESSION_DIRECTIVES
wire.constants.EXPRESSION_DIRECTIVES = [...]
The directives whose value is a Wire expression.
NAME_DIRECTIVES
wire.constants.NAME_DIRECTIVES = [...]
The directives whose value names a variable or a region, and is read as a bare identifier rather than evaluated.
PATH_DIRECTIVES
wire.constants.PATH_DIRECTIVES = [...]
The directives whose value is a template path, read as literal text with
{{ }} interpolation allowed inside it.
FLAG_DIRECTIVES
wire.constants.FLAG_DIRECTIVES = [...]
The directives that take no value at all.
PSEUDO_ELEMENTS
wire.constants.PSEUDO_ELEMENTS = {...}
The pseudo elements Wire accepts as sugar, each mapped to the directive it becomes and the attribute that carries the directive’s value.
These are rewritten into <template> while the source is being
tokenized, so by the time a tree exists there is only one form left to
compile. attribute is nil for a pseudo element that takes no value
of its own.
PSEUDO_FLAGS
wire.constants.PSEUDO_FLAGS = {...}
Attributes a pseudo element may carry that become valueless directives rather than the directive its name maps to.
Only <define name="x" override> uses this today, which is why the
mapping is a single entry rather than a per-element table.
VAR_OPEN
wire.constants.VAR_OPEN = '{{'
Opens an interpolation.
VAR_CLOSE
wire.constants.VAR_CLOSE = '}}'
Closes an interpolation.
CALL_OPEN
wire.constants.CALL_OPEN = '{!'
Opens a template function call.
CALL_CLOSE
wire.constants.CALL_CLOSE = '!}'
Closes a template function call.
ESCAPE_CHAR
wire.constants.ESCAPE_CHAR = '%'
The character that escapes an interpolation, making Wire emit the braces literally instead of reading what is between them.
DEFAULT_EXTENSION
wire.constants.DEFAULT_EXTENSION = '.html'
The extension render() appends when the path it was given does not
name an existing file and carries no extension of its own.
DEFAULT_ROOT
wire.constants.DEFAULT_ROOT = 'templates'
The directory render() resolves template paths against until
set_root() says otherwise, which is a templates directory beside the
current working directory.
DEFAULT_LOOP_NAME
wire.constants.DEFAULT_LOOP_NAME = 'loop'
The variable an x-for publishes its loop metadata under.
MAX_DEPTH
wire.constants.MAX_DEPTH = 64
How deep x-include and x-extend may nest before Wire calls it a
cycle.
A template that includes itself would otherwise recurse until the process died, and the depth a legitimate layout hierarchy reaches is nowhere near this.
2026, Richard Ore and The Zuri Contributors
wire.errors
import wire.errors
wirelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledwire.errors.*needsimport wire.errors.
The errors Wire raises, and the location information they carry.
Every one of them knows which template it came from and, where the problem can be traced to a specific tag, which line and column that tag started at. A template engine that can only say “something is wrong” is not much use once a layout hierarchy is a few files deep, so the location travels with the error rather than being reassembled by whoever catches it.
Classes
WireError
class wire.WireError < Error
The base class of every error Wire raises.
Catching this catches all of them, which is what a web handler wanting to turn any template failure into a 500 page should do.
import wire
catch {
echo wire.render('dashboard')
} as e {
if instance_of(e, wire.WireError) {
echo 'template problem in ${e.path}: ${e.reason}'
}
}
Fields
| Field | Type | Description |
|---|---|---|
path | The template the problem was found in: a file path for a template read from disk, or whatever was passed as… | |
line | The line the offending tag started on, counting from 1, or 0 when the problem cannot be tied to one place… | |
column | The column the offending tag started at, counting from 1, or 0. | |
reason | The problem on its own, without the location that message glues onto the end of it. |
Constructor
wire.WireError(reason: string, path: ?string, line: ?number, column: ?number)
Parameters
reason(string) — What went wrong, phrased without any location; the location is appended for you.path(?string) — The template the problem is in. Defaults to<source>.line(?number) — The line, counting from 1. Omit or pass0when there is no meaningful line.column(?number) — The column, counting from 1.
WireError.location()
wire.WireError.location() -> string
Where the problem is, as path[line,column], or just the path when no
line was recorded.
Returns string
TemplateNotFoundError
class wire.TemplateNotFoundError < WireError
Raised when a template file cannot be found, or when it resolves to somewhere outside the root directory.
A path that climbs out of the root with .. is reported as not found
rather than as a permission problem, so a template cannot be used to
probe the filesystem for what does and does not exist.
Constructor
wire.TemplateNotFoundError(reason: string, path: ?string, line: ?number, column: ?number)
Parameters
reason(string)path(?string)line(?number)column(?number)
TemplateSyntaxError
class wire.TemplateSyntaxError < WireError
Raised while compiling a template, for anything Wire can tell is wrong
without rendering: an unknown directive, an expression that does not
parse, an x-elif with no x-if in front of it, a region defined
twice.
Compilation happens once per template rather than once per render, so these surface the first time a template is used and not on every request after that.
Constructor
wire.TemplateSyntaxError(reason: string, path: ?string, line: ?number, column: ?number)
Parameters
reason(string)path(?string)line(?number)column(?number)
RenderError
class wire.RenderError < WireError
Raised while rendering, for anything that depends on the values a template was given: iterating something that cannot be iterated, a filter that rejects its input, an include hierarchy that never bottoms out.
A variable that was simply not supplied is not one of these. Missing variables render as empty and test as falsy, which is what makes optional sections readable.
Constructor
wire.RenderError(reason: string, path: ?string, line: ?number, column: ?number)
Parameters
reason(string)path(?string)line(?number)column(?number)
2026, Richard Ore and The Zuri Contributors
wire.escape
import wire
wireexposes this aswire.escape, soimport wireis enough and the names are called aswire.escape.*.import wire.escapereaches the same definitions directly.
Turning a value into something safe to write at a particular place in a page.
Escaping HTML is not one operation, it is five. The same string that is
harmless between two tags will end a <script> early, or turn an href
into a code execution, or break out of a stylesheet. Wire knows which of
those places every interpolation sits in, because it compiles a real
parsed tree rather than substituting into text, so it applies the
escaping that place actually needs instead of one approximation
everywhere.
Nothing here is optional or discoverable at render time. A template that
wants unescaped output has to say so with the raw filter, or hand Wire
a Safe value.
Constants
CONTEXT_TEXT
wire.escape.CONTEXT_TEXT = 'text'
Text between tags.
CONTEXT_ATTRIBUTE
wire.escape.CONTEXT_ATTRIBUTE = 'attribute'
An ordinary attribute value.
CONTEXT_URL
wire.escape.CONTEXT_URL = 'url'
An attribute the browser resolves as a URL.
CONTEXT_SCRIPT
wire.escape.CONTEXT_SCRIPT = 'script'
A place the browser reads as JavaScript: the body of a <script>, or
the value of an on* handler attribute.
CONTEXT_STYLE
wire.escape.CONTEXT_STYLE = 'style'
The body of a <style> element.
URL_ATTRIBUTES
wire.escape.URL_ATTRIBUTES = [...]
The attributes whose value a browser resolves as a URL, and which therefore have to be checked for a dangerous scheme rather than merely escaped.
srcset and ping hold several URLs separated by commas or spaces and
are checked entry by entry.
URL_LIST_ATTRIBUTES
wire.escape.URL_LIST_ATTRIBUTES = [...]
URL attributes holding a list of URLs rather than a single one.
DEFAULT_URL_SCHEMES
wire.escape.DEFAULT_URL_SCHEMES = [...]
The URL schemes Wire lets through by default.
A URL with no scheme at all is always allowed, which covers every
relative link, every absolute path and every protocol-relative URL. A
scheme outside this list is replaced with about:blank, because
javascript: and vbscript: in an href are script execution dressed
as a link, and a data: document inherits the origin of the page that
opened it.
Wire.set_url_schemes() replaces this list when an application
genuinely needs another one, app: or mailto+-style custom handlers
being the usual reason.
BLOCKED_URL
wire.escape.BLOCKED_URL = 'about:blank'
What a blocked URL is replaced with. A blocked link stays a link so the page does not fall apart, but it goes nowhere.
STYLE_SAFE
wire.escape.STYLE_SAFE = '/[^-a-zA-Z0-9 \t\n\r#%.,()_\/:;+*!@]/'
The characters a value may contain inside a <style> body.
Everything else is dropped rather than escaped, because a stylesheet has
no escape syntax that would make a < harmless: the tokenizer that
finds </style> runs before CSS is ever parsed.
Functions
escape()
wire.escape.escape(value, context: string, schemes: ?list) -> string
Escapes value for the given context.
A Safe value is written out unchanged in every context. That is the
whole point of it, and it is why raw must only ever be applied to
markup you built yourself.
schemes is the URL scheme allowlist, and is only consulted for
CONTEXT_URL. Passing nil uses DEFAULT_URL_SCHEMES.
Parameters
value(any)context(string) — One of theCONTEXT_*constants.schemes(?list)
Returns string
text()
wire.escape.text(value) -> string
Escapes value for text between tags, turning &, < and > into
character references.
Parameters
value(any)
Returns string
attribute()
wire.escape.attribute(value) -> string
Escapes value for a double quoted attribute value, turning & and "
into character references.
A < needs no escaping inside an attribute and is left alone, which is
what the HTML serialization algorithm does too.
Parameters
value(any)
Returns string
script()
wire.escape.script(value) -> string
Escapes value for a place the browser reads as JavaScript, by encoding it as JSON and then hiding the characters that would end the surrounding element or start a comment.
The result carries its own quotes when the value is a string, so an interpolation in a script must not be quoted by hand:
<script>
var user = {{ name }}; // right: renders as "Ada"
var user = '{{ name }}'; // wrong: renders as '"Ada"'
</script>
U+2028 and U+2029 are escaped as well. They are legal inside a JSON
string but end a line in JavaScript, and an unescaped one turns a valid
script into a syntax error.
Parameters
value(any)
Returns string
style()
wire.escape.style(value) -> string
Escapes value for the body of a <style> element by dropping every
character outside a conservative CSS-safe set.
Dropping rather than escaping is deliberate. There is no escape sequence
that stops a < inside a stylesheet from being found by the HTML
tokenizer looking for </style>, so the only safe answer is for the
character not to be there.
Parameters
value(any)
Returns string
url()
wire.escape.url(value, schemes: ?list) -> string
Escapes value for an attribute the browser resolves as a URL,
replacing it with about:blank when its scheme is not in schemes.
The scheme is read the way a browser reads it: leading and embedded
whitespace and control characters are ignored, and the comparison folds
case, so Java\tscript:alert(1) is caught along with the plain
spelling.
Parameters
value(any)schemes(?list) — The allowlist.nilmeansDEFAULT_URL_SCHEMES.
Returns string
url_list()
wire.escape.url_list(value, schemes: ?list, descriptors: bool) -> string
Escapes value for an attribute holding several URLs, checking each entry’s scheme on its own.
The two attributes that hold a list spell it differently. ping is
whitespace separated and every token in it is a URL. srcset is comma
separated and only the first token of each entry is a URL; what follows
is a descriptor such as 2x or 640w and is left alone. descriptors
picks between them.
Every separator is preserved exactly as it was written, since a srcset
whose spaces went missing is no longer a srcset.
Parameters
value(any)schemes(?list)descriptors(bool) — True forsrcset, false forping.
Returns string
is_url_attribute()
wire.escape.is_url_attribute(name: string) -> bool
Whether name is an attribute the browser resolves as a single URL.
Parameters
name(string)
Returns bool
is_url_list_attribute()
wire.escape.is_url_list_attribute(name: string) -> bool
Whether name is an attribute holding a list of URLs.
Parameters
name(string)
Returns bool
uses_descriptors()
wire.escape.uses_descriptors(name: string) -> bool
Whether a URL list attribute allows a descriptor after each URL, which
srcset does and ping does not.
Parameters
name(string)
Returns bool
is_script_attribute()
wire.escape.is_script_attribute(name: string) -> bool
Whether name is an event handler attribute, whose value a browser reads as JavaScript.
Every one of them starts with on, and treating the whole prefix as
script rather than keeping a list means a handler HTML gains next year
is covered the day it ships.
Parameters
name(string)
Returns bool
attribute_context()
wire.escape.attribute_context(name: string) -> string
The escaping context an attribute named name calls for.
Parameters
name(string)
Returns string
text_context()
wire.escape.text_context(tag: string) -> string
The escaping context text inside an element named tag calls for.
Parameters
tag(string)
Returns string
2026, Richard Ore and The Zuri Contributors
wire.expression
import wire
wireexposes this aswire.expression, soimport wireis enough and the names are called aswire.expression.*.import wire.expressionreaches the same definitions directly.
Wire’s expression language: the thing that sits between {{ and }},
and the thing an x-if or an x-for is given.
It is deliberately a small language rather than an embedded Zuri. There is no assignment, no statement, no way to define anything and no way to reach a global, because a template is a description of a page and everything else belongs in the code that renders it. What is here is what reading a value out of a dictionary and deciding whether to show a section actually needs:
| Kind | Written as |
|---|---|
| literals | 12, 1.5, 'text', "text", true, false, nil |
| collections | [1, 2], { id: 1, name } |
| lookup | user.address.city, items.0, items[index] |
| calls | route('home'), user.display_name() |
| arithmetic | +, -, *, /, % |
| comparison | ==, !=, <, <=, >, >=, in, not in |
| logic | and, or, not (also &&, ` |
| choice | stock > 0 ? 'in stock' : 'sold out' |
| default | nickname ?? name |
| ranges | 0..pages, 1..10 |
| filters | `name |
Truthiness is Wire’s, not Zuri’s: an empty list is false and a negative
number is true. wire.values explains why, and it matters for every
and, or, not and ? here.
Missing values
Reading a name that was never supplied gives nil rather than raising,
and so does reading a key off it. {{ user.address.city }} on a request
with no user renders as nothing at all instead of failing three levels
down, which is what makes an optional section readable. Ask with x-if
when the difference matters.
Names beginning with an underscore cannot be read. Those are private in Zuri, and a template has no business reaching into them.
Constants
KEYWORDS
wire.expression.KEYWORDS = [...]
The words that are part of the language rather than names a template can bind.
LONG_OPERATORS
wire.expression.LONG_OPERATORS = [...]
Operators made of more than one character, longest first so that <= is
never read as < followed by =.
SHORT_OPERATORS
wire.expression.SHORT_OPERATORS = [...]
Operators made of a single character.
Functions
tokenize()
wire.expression.tokenize(source: string, path: string, line: number, column: number) -> list
Splits source into tokens.
path, line and column are only used to place the error when the source does not lex, and name the tag the expression was written on rather than anything inside the expression.
Parameters
source(string)path(string)line(number)column(number)
Returns list
Raises TemplateSyntaxError
parse()
wire.expression.parse(source: string, path: string, line: number, column: number) -> Expression
Compiles source into a syntax tree.
path, line and column describe the tag the expression was written on and are only used to place a syntax error.
import wire
var tree = wire.expression.parse('price * quantity', '<source>', 0, 0)
Parameters
source(string)path(string)line(number)column(number)
Returns Expression
Raises TemplateSyntaxError when source is not a valid
expression.
Classes
Expression
class wire.expression.Expression
The base of every node in a parsed expression.
Nodes are built once, when a template is compiled, and evaluated once per render. They hold no state of their own, so the same compiled template can be rendered from several places at once.
Expression.evaluate()
wire.expression.Expression.evaluate(scope) -> any
Works out this node’s value.
scope is supplied by the renderer and answers the three questions an expression cannot answer for itself: what a name is bound to, what a filter does, and how to report a failure with the position of the tag the expression came from.
Parameters
scope(Scope)
Returns any
Literal
class wire.expression.Literal < Expression
A number, a string, or one of true, false and nil.
Fields
| Field | Type | Description |
|---|---|---|
value | The value, ready to use. |
Constructor
wire.expression.Literal(value)
Parameters
value(any)
Literal.evaluate()
wire.expression.Literal.evaluate(scope) -> any
Parameters
scope(Scope)
Returns any
Variable
class wire.expression.Variable < Expression
A bare name, looked up in the variables the template was rendered with.
Fields
| Field | Type | Description |
|---|---|---|
name | The name being read. |
Constructor
wire.expression.Variable(name: string)
Parameters
name(string)
Variable.evaluate()
wire.expression.Variable.evaluate(scope) -> any
Parameters
scope(Scope)
Returns any
Member
class wire.expression.Member < Expression
A dotted lookup, as in user.name.
Fields
| Field | Type | Description |
|---|---|---|
target | The expression being read from. | |
name | The key, field or method name. |
Constructor
wire.expression.Member(target, name: string)
Parameters
target(Expression)name(string)
Member.evaluate()
wire.expression.Member.evaluate(scope) -> any
Parameters
scope(Scope)
Returns any
Index
class wire.expression.Index < Expression
A bracketed lookup, as in items[index], where the key is itself an
expression.
Fields
| Field | Type | Description |
|---|---|---|
target | The expression being read from. | |
key | The expression giving the key. |
Constructor
wire.expression.Index(target, key)
Parameters
target(Expression)key(Expression)
Index.evaluate()
wire.expression.Index.evaluate(scope) -> any
Parameters
scope(Scope)
Returns any
Call
class wire.expression.Call < Expression
A call, as in route('home') or user.display_name().
Fields
| Field | Type | Description |
|---|---|---|
callee | The expression giving the thing to call. | |
arguments | The argument expressions, in order. | |
label | How the callee was written, for the error message when it turns out not to be callable. |
Constructor
wire.expression.Call(callee, arguments: list, label: string)
Parameters
callee(Expression)arguments(list)label(string)
Call.evaluate()
wire.expression.Call.evaluate(scope) -> any
Parameters
scope(Scope)
Returns any
ListLiteral
class wire.expression.ListLiteral < Expression
A list literal.
Fields
| Field | Type | Description |
|---|---|---|
items | The item expressions, in order. |
Constructor
wire.expression.ListLiteral(items: list)
Parameters
items(list)
ListLiteral.evaluate()
wire.expression.ListLiteral.evaluate(scope) -> list
Parameters
scope(Scope)
Returns list
DictLiteral
class wire.expression.DictLiteral < Expression
A dictionary literal.
Fields
| Field | Type | Description |
|---|---|---|
entries | The entries, as a list of [key, value] expression pairs. |
Constructor
wire.expression.DictLiteral(entries: list)
Parameters
entries(list)
DictLiteral.evaluate()
wire.expression.DictLiteral.evaluate(scope) -> dict
Parameters
scope(Scope)
Returns dict
Unary
class wire.expression.Unary < Expression
not x, !x or -x.
Fields
| Field | Type | Description |
|---|---|---|
operator | The operator: not or -. | |
operand | The expression it applies to. |
Constructor
wire.expression.Unary(operator: string, operand)
Parameters
operator(string)operand(Expression)
Unary.evaluate()
wire.expression.Unary.evaluate(scope) -> any
Parameters
scope(Scope)
Returns any
Binary
class wire.expression.Binary < Expression
An arithmetic or comparison operator.
Fields
| Field | Type | Description |
|---|---|---|
operator | The operator. | |
left | The left operand. | |
right | The right operand. |
Constructor
wire.expression.Binary(operator: string, left, right)
Parameters
operator(string)left(Expression)right(Expression)
Binary.evaluate()
wire.expression.Binary.evaluate(scope) -> any
Parameters
scope(Scope)
Returns any
Logical
class wire.expression.Logical < Expression
and, or or ??, each of which decides whether to evaluate its right
side after looking at its left.
Fields
| Field | Type | Description |
|---|---|---|
operator | The operator: and, or or ??. | |
left | The left operand. | |
right | The right operand. |
Constructor
wire.expression.Logical(operator: string, left, right)
Parameters
operator(string)left(Expression)right(Expression)
Logical.evaluate()
wire.expression.Logical.evaluate(scope) -> any
Parameters
scope(Scope)
Returns any
Conditional
class wire.expression.Conditional < Expression
condition ? consequence : alternative.
Fields
| Field | Type | Description |
|---|---|---|
condition | The expression being tested. | |
consequence | The value when the test passes. | |
alternative | The value when it does not. |
Constructor
wire.expression.Conditional(condition, consequence, alternative)
Parameters
condition(Expression)consequence(Expression)alternative(Expression)
Conditional.evaluate()
wire.expression.Conditional.evaluate(scope) -> any
Parameters
scope(Scope)
Returns any
Filter
class wire.expression.Filter < Expression
A value passed through a filter, as in name|upper.
Fields
| Field | Type | Description |
|---|---|---|
target | The expression giving the value being filtered. | |
name | The filter’s name. | |
arguments | The argument expressions, in order, after the value itself. |
Constructor
wire.expression.Filter(target, name: string, arguments: list)
Parameters
target(Expression)name(string)arguments(list)
Filter.evaluate()
wire.expression.Filter.evaluate(scope) -> any
Parameters
scope(Scope)
Returns any
Token
class wire.expression.Token
One piece of an expression’s source: its kind, its value, and where in the expression it started.
Fields
| Field | Type | Description |
|---|---|---|
type | 'name', 'number', 'string', 'operator' or 'end'. | |
value | The token’s text, or for a string or number its decoded value. | |
offset | How far into the expression the token started, counting from 0. |
Constructor
wire.expression.Token(type: string, value, offset: number)
Parameters
type(string)value(any)offset(number)
Parser
class wire.expression.Parser
Builds a syntax tree from an expression’s tokens.
There is one of these per expression compiled, and it is thrown away once the tree is built.
Fields
| Field | Type | Description |
|---|---|---|
tokens | The tokens left to read. | |
at | How far through them the parser is. | |
source | The expression’s source, for error messages. | |
path | The template the expression came from. | |
line | The line of the tag the expression was written on. | |
column | The column of that tag. |
Constructor
wire.expression.Parser(source: string, path: string, line: number, column: number)
Parameters
source(string)path(string)line(number)column(number)
Parser.parse()
wire.expression.Parser.parse() -> Expression
Parses the whole expression and insists that nothing is left over.
Returns Expression
Raises TemplateSyntaxError
Parser.parse_ternary()
wire.expression.Parser.parse_ternary() -> Expression
condition ? consequence : alternative, which is the loosest binding
thing in the language.
Returns Expression
Parser.parse_coalesce()
wire.expression.Parser.parse_coalesce() -> Expression
a ?? b.
Returns Expression
Parser.parse_range()
wire.expression.Parser.parse_range() -> Expression
a..b, the same range Zuri writes the same way.
Ranges do not chain, since 1..2..3 means nothing.
Returns Expression
Parser.parse_or()
wire.expression.Parser.parse_or() -> Expression
a or b, also spelled a || b.
Returns Expression
Parser.parse_and()
wire.expression.Parser.parse_and() -> Expression
a and b, also spelled a && b.
Returns Expression
Parser.parse_equality()
wire.expression.Parser.parse_equality() -> Expression
a == b and a != b.
Returns Expression
Parser.parse_comparison()
wire.expression.Parser.parse_comparison() -> Expression
The ordering operators, plus in and not in.
Returns Expression
Parser.parse_additive()
wire.expression.Parser.parse_additive() -> Expression
a + b and a - b.
Returns Expression
Parser.parse_multiplicative()
wire.expression.Parser.parse_multiplicative() -> Expression
a * b, a / b and a % b.
Returns Expression
Parser.parse_unary()
wire.expression.Parser.parse_unary() -> Expression
not a, !a and -a.
Returns Expression
Parser.parse_filtered()
wire.expression.Parser.parse_filtered() -> Expression
A value with any filters applied to it.
Filters bind tighter than every operator, so items|length > 3 compares
the length rather than filtering the comparison.
Returns Expression
Parser.parse_filter()
wire.expression.Parser.parse_filter(target) -> Filter
One filter application, in either of the two forms it can take.
Parameters
target(Expression)
Returns Filter
Parser.parse_postfix()
wire.expression.Parser.parse_postfix() -> Expression
A value with any lookups and calls applied to it.
Returns Expression
Parser.parse_arguments()
wire.expression.Parser.parse_arguments() -> list
A comma separated argument list, up to and including its closing parenthesis.
Returns list
Parser.parse_primary()
wire.expression.Parser.parse_primary() -> Expression
A literal, a name, a bracketed expression, or a list or dictionary literal.
Returns Expression
Parser.parse_list()
wire.expression.Parser.parse_list() -> ListLiteral
A list literal, with the opening bracket already consumed.
Returns ListLiteral
Parser.parse_dict()
wire.expression.Parser.parse_dict() -> DictLiteral
A dictionary literal, with the opening brace already consumed.
A key may be a bare name, a quoted string or a bracketed expression. An
entry written as a bare name on its own is the shorthand Zuri uses: { user }
means { user: user }.
Returns DictLiteral
Parser.parse_dict_entry()
wire.expression.Parser.parse_dict_entry() -> list
One key: value pair, or the bare name shorthand.
Returns list
Parser.peek()
wire.expression.Parser.peek() -> Token
The token the parser is looking at.
Returns Token
Parser.take_operator()
wire.expression.Parser.take_operator(value: string) -> bool
Consumes the current token when it is the operator value.
Parameters
value(string)
Returns bool
Parser.take_any_operator()
wire.expression.Parser.take_any_operator(values: list) -> ?string
Consumes the current token when it is any of values, returning which
one it was or nil.
Parameters
values(list)
Returns ?string
Parser.take_keyword()
wire.expression.Parser.take_keyword(word: string) -> bool
Consumes the current token when it is the keyword word.
Parameters
word(string)
Returns bool
Parser.peek_keyword()
wire.expression.Parser.peek_keyword(word: string) -> bool
Whether the current token is the keyword word.
Parameters
word(string)
Returns bool
Parser.peek_keyword_at()
wire.expression.Parser.peek_keyword_at(ahead: number, word: string) -> bool
Whether the token ahead places further on is the keyword word.
Parameters
ahead(number)word(string)
Returns bool
Parser.expect_operator()
wire.expression.Parser.expect_operator(value: string)
Consumes the operator value or reports what was there instead.
Parameters
value(string)
Raises TemplateSyntaxError
Parser.describe_span()
wire.expression.Parser.describe_span() -> string
How the source read from the start of the value just parsed, used to name a callee that turned out not to be callable.
Returns string
Parser.describe()
wire.expression.Parser.describe(token) -> string
A readable rendering of token for an error message.
Parameters
token(Token)
Returns string
Parser.fail()
wire.expression.Parser.fail(reason: string)
Raises a syntax error naming the expression it happened in.
Parameters
reason(string)
Raises TemplateSyntaxError
2026, Richard Ore and The Zuri Contributors
wire.filters
import wire
wireexposes this aswire.filters, soimport wireis enough and the names are called aswire.filters.*.import wire.filtersreaches the same definitions directly.
The filters every Wire template starts with.
A filter takes the value on the left of the | as its first argument
and whatever the template passed as the rest, so the one written
price|round(2) is called as round(price, 2). An argument a template
leaves out arrives as nil, which is why nearly every filter here
states its default in its own doc block rather than insisting the
template spell it out.
Wire.register_filter() adds to this set and can replace anything in
it, so an application that wants date to mean its own thing is free to
say so.
On escaping
Almost every filter here returns a plain string, which Wire then escapes
for wherever it is going. The three that return markup instead are
raw, nl2br and json_script, and each of them says so and says what
it does about the text it was given. A filter that returns markup built
out of untrusted input is how a template engine becomes a security hole,
so the two that build markup escape their input first.
Constants
BUILTIN
wire.filters.BUILTIN = {...}
Every filter Wire starts with, keyed by the name a template calls it by.
Two of them are spelled differently here than the function is: Zuri
already has escape and default on other things, so the functions
carry longer names and the template-facing names live here.
Functions
raw()
wire.filters.raw(value) -> Safe
Marks a value as markup so Wire writes it out without escaping it.
This is the only way to get live HTML out of a variable, and it is a promise that the value is safe where it lands. Applying it to anything a user supplied is a cross-site scripting hole; applying it to markup your own code built is exactly what it is for.
<div class="body">{{ article.rendered_html|raw }}</div>
Parameters
value(any)
Returns Safe
escape_value()
wire.filters.escape_value(value, context) -> Safe
Escapes a value for a context other than the one it is being written into, or escapes a value that was already marked safe.
Wire escapes for the right context on its own, so this is only needed
when a value has to carry escaping it would not otherwise get: a URL
built into an ordinary attribute, say, or markup that reached the
template as Safe but should be shown rather than rendered.
context is 'text', 'attribute', 'url', 'script' or 'style'
and defaults to 'text'.
<code>{{ snippet|escape }}</code>
<a data-target="{{ link|escape('url') }}">go</a>
Parameters
value(any)context(?string)
Returns Safe
Raises ArgumentError when context is not one Wire knows.
upper()
wire.filters.upper(value) -> string
The value in upper case.
{{ code|upper }}
Parameters
value(any)
Returns string
lower()
wire.filters.lower(value) -> string
The value in lower case.
Parameters
value(any)
Returns string
title()
wire.filters.title(value) -> string
The value with the first letter of every word in upper case and the rest in lower case.
Words are separated by whitespace, so o'brien becomes O'brien rather
than O'Brien. Names are harder than a filter can be.
{{ 'jane IS here'|title }} {# Jane Is Here #}
Parameters
value(any)
Returns string
capitalize()
wire.filters.capitalize(value) -> string
The value with its first letter in upper case and the rest left alone.
Unlike title this touches nothing but the first character, which is
what a sentence wants.
{{ 'hello THERE'|capitalize }} {# Hello THERE #}
Parameters
value(any)
Returns string
trim()
wire.filters.trim(value) -> string
The value with leading and trailing whitespace removed.
Parameters
value(any)
Returns string
truncate()
wire.filters.truncate(value, length, suffix) -> string
The value cut down to length characters, with suffix put on the end when anything was actually cut.
length counts the characters kept, not including the suffix. suffix defaults to a single ellipsis. A value already short enough is returned untouched, suffix and all left off.
{{ article.summary|truncate(80) }}
{{ article.summary|truncate(80, ' [more]') }}
Parameters
value(any)length(number)suffix(?string)
Returns string
Raises ArgumentError when length is missing or negative.
replace()
wire.filters.replace(value, search, replacement) -> string
The value with every occurrence of search replaced by replacement.
search is matched literally, never as a regular expression, so a value
full of punctuation cannot turn into a pattern by accident.
replacement defaults to an empty string, which makes |replace(',') a
way to strip something out.
Parameters
value(any)search(string)replacement(?string)
Returns string
Raises ArgumentError when search is missing or empty.
lpad()
wire.filters.lpad(value, width, fill) -> string
The value padded on the left with fill until it is width characters long.
fill defaults to a space. A value already at least width long is returned untouched.
Parameters
value(any)width(number)fill(?string)
Returns string
Raises ArgumentError when width is missing or not a number.
rpad()
wire.filters.rpad(value, width, fill) -> string
The value padded on the right with fill until it is width characters long.
Parameters
value(any)width(number)fill(?string)
Returns string
Raises ArgumentError when width is missing or not a number.
repeat()
wire.filters.repeat(value, count) -> string
The value repeated count times.
A count of zero gives an empty string.
Parameters
value(any)count(number)
Returns string
Raises ArgumentError when count is missing or negative.
nl2br()
wire.filters.nl2br(value) -> Safe
The value with its line breaks turned into <br> elements.
The text is escaped first and the result is marked as markup, so this is
safe on anything a user typed. Both \n and \r\n count as a line
break.
<p>{{ comment.body|nl2br }}</p>
Parameters
value(any)
Returns Safe
strip_tags()
wire.filters.strip_tags(value) -> string
The value with every HTML tag removed, leaving only its text.
The markup is really parsed rather than pattern-matched away, so <p title="a>b">x</p>
gives x and not b">x. The result is plain text and is escaped like
anything else when it is written out.
<meta name="description" content="{{ article.body|strip_tags|truncate(150) }}">
Parameters
value(any)
Returns string
slug()
wire.filters.slug(value) -> string
The value as a lowercase, hyphen separated slug.
Runs of anything that is not a letter or a digit become a single hyphen, and hyphens at either end are trimmed. Non-ASCII letters are kept, so a slug can still be readable in a language that needs them.
<a href="/posts/{{ post.title|slug }}">{{ post.title }}</a>
Parameters
value(any)
Returns string
url_encode()
wire.filters.url_encode(value) -> string
The value percent encoded for use inside a URL.
Parameters
value(any)
Returns string
json()
wire.filters.json(value) -> string
The value as JSON.
The result is plain text, so writing it into a page escapes it like
anything else. Inside a <script> Wire already encodes every
interpolation as JSON, which makes this filter unnecessary there.
Parameters
value(any)
Returns string
json_script()
wire.filters.json_script(value, id) -> Safe
The value as JSON wrapped in a <script type="application/json">
element, ready to be read back by a script on the page.
id becomes the element’s id so the script can find it, and defaults to leaving the id off. This is the safe way to hand data to the browser: the JSON never touches an executable context, so no value in it can become code.
{{ initial_state|json_script('state') }}
<script>
var state = JSON.parse(document.getElementById('state').textContent)
</script>
Parameters
value(any)id(?string)
Returns Safe
abs()
wire.filters.abs(value) -> number
The value without its sign.
Parameters
value(any)
Returns number
Raises TypeError when the value is not a number.
round()
wire.filters.round(value, places) -> number
The value rounded to places decimal places.
places defaults to 0, which rounds to a whole number. Halves round
away from zero, so 2.5 becomes 3 and -2.5 becomes -3.
{{ order.total|round(2) }}
Parameters
value(any)places(?number)
Returns number
Raises TypeError when the value is not a number.
floor()
wire.filters.floor(value) -> number
The largest whole number at or below the value.
Parameters
value(any)
Returns number
Raises TypeError when the value is not a number.
ceil()
wire.filters.ceil(value) -> number
The smallest whole number at or above the value.
Parameters
value(any)
Returns number
Raises TypeError when the value is not a number.
number_format()
wire.filters.number_format(value, places, point, separator) -> string
The value written out with thousands separated and a fixed number of decimal places.
places defaults to 0, point to '.' and separator to ',',
which is the convention most of the English speaking world uses. Passing
the other two the other way round gives the European one.
{{ 1234567.891|number_format(2) }} {# 1,234,567.89 #}
{{ 1234567.891|number_format(2, ',', '.') }} {# 1.234.567,89 #}
Parameters
value(any)places(?number)point(?string)separator(?string)
Returns string
Raises TypeError when the value is not a number.
filesize()
wire.filters.filesize(value, binary) -> string
A byte count written the way a person reads it.
binary chooses the units: left out or false gives the decimal ones a
disk is sold in (kB of 1000 bytes), true gives the binary ones memory
is measured in (KiB of 1024). Values below a kilobyte are written as a
plain count of bytes.
{{ upload.size|filesize }} {# 1.4 MB #}
{{ upload.size|filesize(true) }} {# 1.3 MiB #}
Parameters
value(any)binary(?bool)
Returns string
Raises TypeError when the value is not a number.
length()
wire.filters.length(value) -> number
How many entries the value has.
Works on a string, a list, a dictionary or a byte string. nil has a
length of zero rather than being an error, so x-if="items|length"
reads correctly for a variable that was never supplied.
Parameters
value(any)
Returns number
Raises TypeError when the value is not something with a length.
first()
wire.filters.first(value) -> any
The first entry, or nil when there is none.
Parameters
value(any)
Returns any
Raises TypeError when the value cannot be indexed.
last()
wire.filters.last(value) -> any
The last entry, or nil when there is none.
Parameters
value(any)
Returns any
Raises TypeError when the value cannot be indexed.
join()
wire.filters.join(value, glue) -> string
The entries joined into one string with glue between them.
glue defaults to an empty string. Each entry is rendered the way it would be if it were written on its own, so a list of numbers joins without any ceremony.
{{ tags|join(', ') }}
Parameters
value(any)glue(?string)
Returns string
Raises TypeError when the value is not a sequence.
sort()
wire.filters.sort(value, key) -> list
The entries in ascending order.
key names a field to sort by, for a list of dictionaries or instances; leaving it out sorts the entries themselves. Sorting is stable, and the original is not changed.
<li x-for="users|sort('name')" x-value="user">{{ user.name }}</li>
Parameters
value(any)key(?string)
Returns list
Raises TypeError when the value is not a sequence.
reverse()
wire.filters.reverse(value) -> any
The entries in the opposite order, or a string backwards.
Parameters
value(any)
Returns any
Raises TypeError when the value is not a sequence.
unique()
wire.filters.unique(value) -> list
The entries with later duplicates removed, keeping the first of each.
Parameters
value(any)
Returns list
Raises TypeError when the value is not a sequence.
keys()
wire.filters.keys(value) -> list
A dictionary’s keys, in insertion order.
Parameters
value(any)
Returns list
Raises TypeError when the value is not a dictionary.
values()
wire.filters.values(value) -> list
A dictionary’s values, in insertion order.
Parameters
value(any)
Returns list
Raises TypeError when the value is not a dictionary.
slice()
wire.filters.slice(value, start, end) -> any
The entries from start up to but not including end.
end defaults to the end of the sequence. A negative position counts back from the end, and a range that falls outside the sequence gives back whatever part of it does overlap rather than raising.
<li x-for="posts|slice(0, 5)" x-value="post">{{ post.title }}</li>
Parameters
value(any)start(number)end(?number)
Returns any
Raises TypeError when the value is not a sequence.
sum()
wire.filters.sum(value, key) -> number
The entries added together.
key names a field to add up, for a list of dictionaries or instances.
An empty sequence sums to 0.
<p>Total: {{ items|sum('price')|number_format(2) }}</p>
Parameters
value(any)key(?string)
Returns number
Raises TypeError when an entry is not a number.
split()
wire.filters.split(value, separator) -> list
The value split into a list on separator.
separator is matched literally and defaults to whitespace, which splits on any run of it.
Parameters
value(any)separator(?string)
Returns list
default_to()
wire.filters.default_to(value, fallback) -> any
fallback when the value is falsy, otherwise the value.
Falsy here is Wire’s falsy: nil, false, zero, an empty string and an
empty collection. A negative number is not falsy and is kept. Use ??
in an expression when only a missing value should fall back and a zero
should stand.
<p>{{ user.nickname|default('friend') }}</p>
Parameters
value(any)fallback(any)
Returns any
empty()
wire.filters.empty(value) -> bool
Whether the value has nothing in it.
nil is empty, an empty string and an empty collection are empty, and a
number never is. That last part is what separates this from plain
falsiness: 0|empty is false where 0 on its own is falsy.
<p x-if="results|empty">Nothing found.</p>
Parameters
value(any)
Returns bool
is()
wire.filters.is(value, expected) -> bool
Whether the value equals expected.
Kept from the first version of Wire, where it was the only way to
compare anything. x-if="status == 'active'" says the same thing and
reads better, so prefer that in new templates.
Parameters
value(any)expected(any)
Returns bool
not()
wire.filters.not(value, expected) -> bool
Whether the value differs from expected.
Parameters
value(any)expected(any)
Returns bool
date()
wire.filters.date(value, format) -> string
A date written out with the given format.
The value may be a date.Date, a Unix timestamp in seconds, or a string
in any of the formats date.parse() understands. format uses the same
directives as Date.format() and defaults to 'Y-m-d H:i:s'.
<time datetime="{{ post.created|date('Y-m-d') }}">
{{ post.created|date('jS F Y') }}
</time>
Parameters
value(any)format(?string)
Returns string
Raises TypeError when the value is not something Wire can read as
a date.
2026, Richard Ore and The Zuri Contributors
wire.loader
import wire.loader
wiredoes not re-export this module, so it is reached only by importing it directly.
Turning the path written in an x-include into a file on disk, and
refusing to when it points somewhere it should not.
The root is a boundary, not a starting point
Every template path resolves inside the root directory and nowhere else.
A path that climbs out of it with .., and an absolute path that names
somewhere else entirely, are both refused rather than followed.
That matters because template paths are not always written by the person
who wrote the template. The moment a path is built out of anything a
request supplied, as x-include="themes/{{ theme }}/head" does, an
unbounded loader turns an include into a way to read any file the
process can reach. Wire treats the root as a boundary so that the worst
a hostile value can do is fail to find a template.
Symbolic links are followed and then checked, so a link inside the root pointing outside it is refused too.
Classes
Loader
class wire.loader.Loader
Finds template files under one root directory.
Fields
| Field | Type | Description |
|---|---|---|
root | The directory every path resolves inside. | |
extension | The extension tried when the path as written names no file. |
Constructor
wire.loader.Loader(root: string, extension: ?string)
Parameters
root(string)extension(?string)
Loader.resolve()
wire.loader.Loader.resolve(path: string, from: ?string, line: ?number, column: ?number) -> string
The file path names, as an absolute path.
The path is tried as written first and then with the configured
extension appended, so render('home') finds home.html while
render('home.txt') finds exactly that.
from and line and column describe where the include was written and are only used to place the error.
Parameters
path(string)from(?string)line(?number)column(?number)
Returns string
Raises TemplateNotFoundError when there is no such file, or when
the path resolves outside the root.
Loader.read()
wire.loader.Loader.read(full: string) -> string
The contents of the file at full.
Parameters
full(string)
Returns string
Raises TemplateNotFoundError when the file cannot be read.
Loader.fingerprint()
wire.loader.Loader.fingerprint(full: string) -> ?string
What the file at full looks like now, for telling a cached template apart from a changed one.
Both the modification time and the size are used, because a file
rewritten within the same second still almost always changes length. A
file that cannot be stated fingerprints as nil, which makes the cache
treat it as changed and read it again.
Parameters
full(string)
Returns ?string
2026, Richard Ore and The Zuri Contributors
wire.normalize
import wire
wireexposes this aswire.normalize, soimport wireis enough and the names are called aswire.normalize.*.import wire.normalizereaches the same definitions directly.
Rewriting Wire’s pseudo elements into something HTML’s own tree construction will not move, drop or reshape.
Why this exists
<include>, <extend>, <declare> and <define> are not HTML
elements, and a standards-following parser has firm opinions about
elements it does not know. Inside <head> one is pushed out into
<body>. Inside a table one is foster parented out in front of the
table. Inside a <select> one is dropped on the floor. And written as
<include path="x" />, the trailing slash means nothing on an unknown
element, so it swallows the whole rest of the document as its children.
All four of those are places a template genuinely wants an include: a
shared block of <meta> tags, a partial holding table rows, a partial
holding options. So Wire does not hand its own tag names to the parser
at all. While the source is being tokenized, every pseudo element is
turned into a <template> carrying the directive it means, and
<template> is the one element the standard lets through untouched
everywhere.
The rewrite happens on the token stream rather than on the text, so it
inherits the tokenizer’s own understanding of comments, of raw text
inside <script> and <style>, and of quoting. A <include> written
inside a comment or inside a script stays exactly where it was.
What else it notices
A template may be a whole page or a fragment of one, and the tree
builder gives you <html>, <head> and <body> either way. Telling
the two apart afterwards is guesswork, so the flags are collected here
while the real tokens go past: whether the source actually spelled a
doctype, an <html>, a <head> or a <body>. The compiler emits each
of those only when the author wrote it.
Functions
parse()
wire.normalize.parse(source: string, context: ?string) -> dict
Parses source into a tree with Wire’s pseudo elements already rewritten.
context names the element the source is being parsed inside. Pass
nil to parse it as a whole document, which is what a template being
rendered on its own is; pass a tag name to parse it as a fragment, so
that a partial holding table rows keeps its rows when it is included
inside a table.
Returns a dictionary of:
root: theDocumentfor a document parse, or theDocumentFragmentholding the parsed nodes for a fragment parse.document: true when the parse was a document parse.saw_doctype,saw_html,saw_head,saw_body: whether the source really contained each, as opposed to the parser having supplied it.errors: the parse errors the tokenizer and tree builder found.
import wire
var parsed = wire.normalize.parse('<include path="nav.html" />', nil)
echo parsed.root.body().inner_html()
# <template x-include="nav.html"></template>
Parameters
source(string)context(?string)
Returns dict
Classes
Normalizer
class wire.normalize.Normalizer
A tokenizer that rewrites Wire’s pseudo elements on the way past.
This stands where a html.Tokenizer normally stands and answers
everything the tree builder asks of one, so html.TreeBuilder cannot
tell the difference.
Fields
| Field | Type | Description |
|---|---|---|
cdata_ok | Whether the tree builder will accept a CDATA section at the current insertion point. | |
errors | The parse errors the underlying tokenizer has collected. | |
saw_doctype | True once a <!DOCTYPE> has gone past. | |
saw_html | True once an <html> start tag has gone past. | |
saw_head | True once a <head> start tag has gone past. | |
saw_body | True once a <body> start tag has gone past. |
Constructor
wire.normalize.Normalizer(source)
Parameters
source(Tokenizer)
Normalizer.next_token()
wire.normalize.Normalizer.next_token() -> Token
The next token, rewritten when it names a pseudo element.
Returns Token
Normalizer.set_state()
wire.normalize.Normalizer.set_state(state: string)
Passes the tree builder’s tokenizer state change along.
Parameters
state(string)
Normalizer.set_last_start_tag()
wire.normalize.Normalizer.set_last_start_tag(name: string)
Passes the tree builder’s record of the last start tag along, which is
how the tokenizer knows whether a </script> it is looking at closes
the script it is inside.
Parameters
name(string)
2026, Richard Ore and The Zuri Contributors
wire.render
import wire.render
wiredoes not re-export this module, so it is reached only by importing it directly.
Walking a compiled template and writing the page out.
Nothing is parsed here and no markup is reassembled from text. The compiler already decided what every instruction is and what escaping every interpolation needs, so rendering is a walk that evaluates expressions and appends strings, and a value can never turn into structure on its way to the page.
The renderer is also the scope an expression is evaluated against: it is what answers what a name is bound to, what a filter does, and where to point when something goes wrong.
Classes
Frame
class wire.render.Frame
One level of variables, pointing at the level around it.
A loop pushes one of these rather than copying everything it can see, so iterating a long list costs one small dictionary per iteration instead of a copy of the whole page’s variables.
Fields
| Field | Type | Description |
|---|---|---|
variables | The variables bound at this level. | |
outer | The level around this one, or nil at the outermost. |
Constructor
wire.render.Frame(variables: dict, outer)
Parameters
variables(dict)outer(?Frame)
Frame.lookup()
wire.render.Frame.lookup(name: string) -> any
What name is bound to at this level or any level around it, or nil
when nothing binds it.
Parameters
name(string)
Returns any
Definition
class wire.render.Definition
One definition of a region, and what to render it against.
A region’s body does not always belong to the template placing it. An
extending template’s x-define is written against the same variables
the base is, so it renders in whatever scope is current. An include’s
children are written at the call site and have to render in the scope
they were written in, or <include path="card"><p>{{ orders|length }} </p></include>
would be asking the card about the caller’s orders and getting nothing.
Fields
| Field | Type | Description |
|---|---|---|
body | The instructions to render. | |
frame | The scope to render them in, or nil to use whatever is current. | |
path | The template the body was written in, for error messages. |
Constructor
wire.render.Definition(body: list, frame, path: string)
Parameters
body(list)frame(?Frame)path(string)
Renderer
class wire.render.Renderer
Renders compiled templates.
One of these is built per render and thrown away afterwards, so a compiled template can be rendered from several places at once without them treading on each other.
Fields
| Field | Type | Description |
|---|---|---|
environment | The Wire the render belongs to, which owns the filters, the registered elements, the loader and the compile… | |
path | The template currently being rendered, for error messages. | |
pieces | The pieces of the page so far, joined at the end. | |
frame | The variables in scope right now. | |
blocks | The regions an extending template supplied, keyed by name. | |
overridden | The definitions an x-super would reach, while a region is being rendered. | |
depth | How many includes and extends deep the render is. | |
line | The line of the instruction being rendered, for error messages. | |
column | The column of that instruction. |
Constructor
wire.render.Renderer(environment)
Parameters
environment(Wire)
Renderer.run()
wire.render.Renderer.run(template, variables: dict) -> string
Renders template with variables and returns the page.
Parameters
template(Template)variables(dict)
Returns string
Raises WireError
Renderer.template()
wire.render.Renderer.template(template, variables: dict, supplied: dict, depth: number)
Renders template, following whatever it extends and placing the regions it and its children define.
supplied holds regions coming from outside the inheritance chain, which is how an include hands its own children to the template it is including.
Parameters
template(Template)variables(dict)supplied(dict)depth(number)
Raises WireError
Renderer.nodes()
wire.render.Renderer.nodes(nodes: list)
Renders a list of instructions in order.
Parameters
nodes(list)
Renderer.node()
wire.render.Renderer.node(node)
Renders one instruction.
Parameters
node(Instruction)
Renderer.definition()
wire.render.Renderer.definition(definition)
Renders one definition of a region, in its own scope when it has one.
Parameters
definition(Definition)
Renderer.plain()
wire.render.Renderer.plain(segments: list) -> string
A run of segments joined with no escaping at all, for a template path.
Parameters
segments(list)
Returns string
Renderer.lookup()
wire.render.Renderer.lookup(name: string) -> any
What name is bound to, for an expression being evaluated.
Parameters
name(string)
Returns any
Renderer.apply_filter()
wire.render.Renderer.apply_filter(name: string, value, arguments: list) -> any
Runs the filter name over value, for an expression being evaluated.
Parameters
name(string)value(any)arguments(list)
Returns any
Raises RenderError when there is no such filter, or when the
filter rejects what it was given.
Renderer.fail()
wire.render.Renderer.fail(reason: string)
Stops the render, pointing at the instruction currently being rendered.
Parameters
reason(string)
Raises RenderError
2026, Richard Ore and The Zuri Contributors
wire.values
import wire.values
wirelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledwire.values.*needsimport wire.values.
How Wire reads the values a template is given: what counts as true, what a value looks like once it reaches the page, and how a string says that it is already markup and must not be escaped again.
Constants
MAX_ARGUMENTS
wire.values.MAX_ARGUMENTS = 8
The most arguments a filter or a template function can be called with.
Zuri has no way to spread a list into a call, so an argument list built at render time has to be handed over one position at a time and the positions have to be written out. Eight is far past what any filter has ever wanted and stops the dispatch below from growing without end.
Functions
safe()
wire.safe(value) -> Safe
Marks value as markup that Wire must not escape.
A Safe passed back in is returned unchanged rather than wrapped twice.
Anything that is not already a string is rendered with stringify()
first, so safe(nil) is an empty string rather than the word “nil”.
Parameters
value(any)
Returns Safe
is_safe()
wire.is_safe(value) -> bool
True when value is a Safe.
Parameters
value(any)
Returns bool
unwrap()
wire.values.unwrap(value) -> any
Strips the Safe wrapper off value, leaving anything else alone.
Filters work on plain values, so this runs on the way into one and the filter decides for itself whether its result is markup.
Parameters
value(any)
Returns any
stringify()
wire.stringify(value) -> string
What value looks like once it reaches the page, before escaping.
nil renders as an empty string rather than as the word “nil”, which is
what lets an optional variable be dropped into a page without a guard
around it. Everything else is rendered by its own to_string().
Parameters
value(any)
Returns string
truthy()
wire.truthy(value) -> bool
Whether value counts as true in an x-if, an x-not, or a boolean
operator inside an expression.
Wire deliberately does not reuse Zuri’s own truthiness here, because one
of Zuri’s answers is wrong for a template: Zuri treats an empty list and
an empty dictionary as true, so x-if="items" would render a section
for a search that found nothing. Wire treats an empty collection as
false.
Everything else agrees with Zuri: nil and false are false, zero and
NaN are false while every other number is true, an empty string is
false, and anything Wire has no special knowledge of (an instance, a
function, a range) is true.
A variable that was never supplied arrives here as nil, which is why
an optional section can be written as x-if="user" with nothing else
around it.
Parameters
value(any)
Returns bool
is_empty()
wire.values.is_empty(value) -> bool
Whether value has nothing in it, for the empty filter and for
anything else that wants the question asked of a collection rather than
of a value in general.
nil is empty. A number is never empty, not even zero; that is a
question about truthiness, not about emptiness.
Parameters
value(any)
Returns bool
is_blank()
wire.values.is_blank(text: string) -> bool
Whether text is empty or is nothing but whitespace.
Parameters
text(string)
Returns bool
compare()
wire.values.compare(a, b) -> number
Orders a before, with or after b, returning -1, 0 or 1.
Zuri’s ordering operators only take numbers, so anything a template sorts or compares that is not one has to be ordered here. Strings are compared a character at a time by code point, which puts them in the order a person expects for one alphabet and in a defined order for everything else. Two values of different kinds are ordered by how they render, which is arbitrary but stable, and stable is what a sort actually needs.
nil sorts before everything, so a record missing the field being
sorted on still appears rather than dropping out of the list.
Parameters
a(any)b(any)
Returns number
invoke()
wire.values.invoke(target, arguments: list) -> any
Calls target with arguments spread into its parameters.
Passing fewer arguments than the function declares leaves the rest
nil, which is how a filter written as def round(value, places) can
be used as a bare |round.
Parameters
target(callable)arguments(list)
Returns any
Raises ArgumentError when there are more than MAX_ARGUMENTS.
type_name()
wire.values.type_name(value) -> string
Wire’s name for value’s type, used in error messages so that a complaint reads “expected a list, got a number” rather than naming an internal class.
Parameters
value(any)
Returns string
Classes
Safe
class wire.Safe
A string that is already markup and must be written out as it is.
Wire escapes everything it interpolates. That is the right default and
it is not negotiable per-variable from the outside, so the only way to
get live markup into a page is to say so explicitly, either with the
raw filter in a template or by handing Wire one of these from Zuri
code.
import wire
var tpl = wire.wire()
# Escaped, because that is the default.
echo tpl.render_string('{{ note }}', { note: '<b>hi</b>' })
# <b>hi</b>
# Not escaped, because the value says it is markup.
echo tpl.render_string('{{ note }}', { note: wire.safe('<b>hi</b>') })
# <b>hi</b>
Wrapping a value is a promise that it is safe in an HTML context. Wrapping something that came from a user is how a template engine becomes a cross-site scripting hole, so wrap the markup you built, never the input you received.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
value | The markup this value carries. |
Constructor
wire.Safe(value: string)
Parameters
value(string)
Safe.to_string()
wire.Safe.to_string() -> string
The markup, unchanged.
Returns string
2026, Richard Ore and The Zuri Contributors
url
import url
This module provides classes and functions for parsing, building,
resolving and processing URLs (and URL-like identifiers, such as
mailto: and tel: links) per RFC
3986.
The scope of this module is not limited to HTTP or any single scheme: it
will happily parse ftp://, ssh://, mailto:, or any scheme it has
never heard of, and only makes scheme-specific decisions (like a default
port) where the scheme is one this module actually recognizes.
Constructing a URL
%> import url
%> var link = url.Url('https', 'example.com', 9000)
%> link.absolute_url()
'https://example.com:9000'
Parsing a URL
The parse() function turns a URL string into a Url instance.
%> var link = url.parse('https://example.com:9000/path?q=1#frag')
%> link.scheme
'https'
%> link.host
'example.com'
%> link.port
9000
%> link.path
'/path'
%> link.query
'q=1'
%> link.hash
'frag'
Resolving relative URLs
resolve() implements RFC 3986’s relative-reference resolution
algorithm, the same logic a browser uses to turn a relative href on a
page into an absolute link.
%> var base = url.parse('https://example.com/a/b/c')
%> base.resolve('../d').to_string()
'https://example.com/a/d'
Query strings
query_params() decodes a URL’s query string into a dictionary of name -> [values]
(a name can legally appear more than once in a query string, so every
value is always a list).
%> url.parse('https://example.com?a=1&a=2&b=3').query_params()
{a: [1, 2], b: [3]}
The url API
Every public name in url, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
url.Url | class | The Url class represents a parsed (or hand-built) URL and provides methods for inspecting, normalizing,… |
url.UrlMalformedError | class | Raised by parse() when strict is true and the given string cannot be interpreted as a well-formed URL. |
url.decode | function | Decodes URL-encoded string. |
url.encode | function | URL-encodes a string. |
url.has_authority | function | Returns true if scheme (case-insensitively) allows the :// authority form directly; false for opaque… |
url.parse | function | Parses given url string into a Url object. |
url.parse_query | function | Decodes a raw query string (without a leading ?) into a dictionary mapping each parameter name to a list of… |
url.remove_dot_segments | function | Removes . and .. path segments from path, per [RFC 3986… |
Functions
remove_dot_segments()
url.remove_dot_segments(path: string) -> string
Removes . and .. path segments from path, per RFC 3986
§5.2.4. This is what
keeps resolve() from leaking a base URL’s discarded segments into its
result, e.g. /a/b/../c normalizes to /a/c.
Parameters
path(string)
Returns string
encode()
url.encode(url: string, strict: ?bool) -> string
URL-encodes a string.
This function is convenient when encoding a string to be used in a query part of a URL, as a convenient way to pass variables to the next page. Every byte of the string’s UTF-8 encoding that isn’t one of the reserved/unreserved characters defined by RFC 3986 is percent-encoded, so this is safe to use on text containing multi-byte characters.
If strict mode is enabled, space character is encoded with the percent (%) sign in order to conform with RFC 3986. Otherwise, is is encoded with the plus (+) sign in order to align with the default encoding used by modern browsers.
Parameters
url(string)strict(?bool) — Default value isfalse
Returns string
decode()
url.decode(url: string) -> string
Decodes URL-encoded string. This function decodes any %## encoding in the given string and plus symbols (‘+’) to a space character.
Parameters
url(string)
Returns string
parse_query()
url.parse_query(query: string) -> dict
Decodes a raw query string (without a leading ?) into a dictionary
mapping each parameter name to a list of its values. Equivalent to
parse('?' + query).query_params(), but usable directly on a query
string with no surrounding url.
%> url.parse_query('a=1&a=2&b=3')
{a: [1, 2], b: [3]}
Parameters
query(string)
Returns dict
has_authority()
url.has_authority(scheme: string) -> bool
Returns true if scheme (case-insensitively) allows the ://
authority form directly; false for opaque schemes such as mailto or
tel that address their content by an opaque string instead of a host.
Parameters
scheme(string)
Returns bool
parse()
url.parse(url: string, strict: ?bool) -> Url
Parses given url string into a Url object. If the strict argument is set
to true, the parser will raise an UrlMalformedError when it
encounters a malformed url; otherwise it makes a best effort and leaves
whatever it couldn’t confidently parse as nil.
Parameters
url(string)strict(?bool) — Default value isfalse
Returns Url
Raises UrlMalformedError if strict is true and the url is
malformed.
Classes
UrlMalformedError
class url.UrlMalformedError < Error
Raised by parse() when strict is true and the given string cannot
be interpreted as a well-formed URL.
Constructor
url.UrlMalformedError(message)
Parameters
message(string)
Url
class url.Url
The Url class represents a parsed (or hand-built) URL and provides methods for inspecting, normalizing, resolving and re-serializing it.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode()
Fields
| Field | Type | Description |
|---|---|---|
scheme | The url scheme e.g. http, https, ftp, tcp etc. nil for a schemeless reference such as a bare path. | |
host | The host information contained in the url. | |
port | The port contained in the url, as a number. | |
path | The path of the URL. | |
hash | The url’s fragment/hash, without the leading #. | |
query | The url’s raw query string, without the leading ?. | |
username | The username portion of the url’s userinfo, if any. | |
password | The password portion of the url’s userinfo, if any. | |
has_slash | true if the url contains the :// (or bare //) authority marker. | |
empty_path | true if path is / only because no path was actually present in the original url (it was implied);… |
Constructor
url.Url(scheme: ?string, host: ?string, port: ?number, path: ?string, query: ?string, hash: ?string, username: ?string, password: ?string, has_slash: ?bool, empty_path: ?bool)
Parameters
scheme(?string)host(?string)port(?number)path(?string)query(?string)hash(?string)username(?string)password(?string)has_slash(?bool)empty_path(?bool)
Url.clone()
url.Url.clone() -> Url
Returns a clone of the current url.
Returns Url
Url.is_absolute()
url.Url.is_absolute() -> bool
Returns true if the url is absolute (has a scheme), false if it is a
relative reference (e.g. a bare path).
Returns bool
Url.default_port()
url.Url.default_port() -> ?number
Returns the standard, well-known port for the url’s scheme (e.g. 80
for http, 443 for https), or nil if the scheme is unset or not
one this module recognizes.
Returns ?number
Url.effective_port()
url.Url.effective_port() -> ?number
Returns port if the url specifies one, otherwise falls back to
default_port().
Returns ?number
Url.query_params()
url.Url.query_params() -> dict
Decodes the url’s query string into a dictionary mapping each parameter
name to a list of its values, since a query string can legally repeat
the same name more than once. A name with no = (e.g. ?flag) decodes
to a single empty-string value.
%> url.parse('https://example.com?a=1&a=2&b=3').query_params()
{a: [1, 2], b: [3]}
Returns dict
Url.get_param()
url.Url.get_param(name: string, fallback) -> any
Returns the first value of query parameter name, or fallback if it
isn’t present. A convenience shortcut over query_params() for the
common case of a parameter that only appears once.
Parameters
name(string)fallback(any) — Default value isnil.
Returns any
Url.normalize()
url.Url.normalize() -> Url
Returns a new url with . and .. path segments resolved away, per RFC
3986 §5.2.4. Every other field is left untouched.
%> url.parse('https://example.com/a/b/../c').normalize().path
'/a/c'
Returns Url
Url.resolve()
url.Url.resolve(reference) -> Url
Resolves reference (a relative or absolute url string, or another
Url instance) against this url as a base, per RFC 3986
§5, the same algorithm a
browser uses to turn a page’s relative hrefs into absolute links.
%> var base = url.parse('https://example.com/a/b/c')
%> base.resolve('../d').to_string()
'https://example.com/a/d'
%> base.resolve('/x/y').to_string()
'https://example.com/x/y'
%> base.resolve('?q=1').to_string()
'https://example.com/a/b/c?q=1'
Parameters
reference(string|Url)
Returns Url
Url.authority()
url.Url.authority() -> string
Returns the url authority.
The authority component is preceded by a double slash (“//”) and is terminated by the next slash (“/”), question mark (“?”), or number sign (“#”) character, or by the end of the URI.
Returns string
Note: mailto and other opaque schemes have no authority. For this reason, they return an empty string as authority.
Url.host_for_display()
url.Url.host_for_display() -> string
Returns host, wrapped in [ ] brackets when it looks like an IPv6
literal, matching how it must appear when embedded back into a url
string. Plain hostnames and IPv4 addresses are returned as-is.
Returns string
Url.host_is_ipv4()
url.Url.host_is_ipv4() -> bool
Returns true if the host of the url is a valid ipv4 address and false otherwise.
Returns bool
Url.host_is_ipv6()
url.Url.host_is_ipv6() -> bool
Returns true if the host of the url is a valid ipv6 address and false otherwise.
Returns bool
Url.absolute_url()
url.Url.absolute_url() -> string
Returns absolute url string of the url object.
Returns string
Url.to_string()
url.Url.to_string() -> string
Returns a string representation of the url object. This will only be the same as the absolute url if the original string is an absolute url.
Returns string
Url.equals()
url.Url.equals(other) -> bool
Returns true if other is a Url whose string form is identical to
this one’s. Note that == does not call this: it always falls back to
identity comparison for instances, so use a.equals(b) rather than a == b
to compare two urls by value.
Parameters
other(any)
Returns bool
2021, Richard Ore and Zuri contributors
mime
import mime
This module provides functions that allow easy mime type detection from files. It offers support for detecting file type based on name or file headers and it is completely extensible so that you can add declarations for your own custom file types.
See defined functions for example.
The mime API
Every public name in mime, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
mime.MimeFormat | class | Mime format representation class. |
mime.detect | function | Performs mimetype detection on a file. |
mime.detect_from_header | function | Detects the mimetype of a file based on it’s file header. |
mime.detect_from_name | function | Detects the mimetype of a file based on the extension defined in it’s path. |
mime.extend | function | Extends the mime module with support for files with the given extension as defined in the given format. |
mime.mime_to_extension | function | Looks up the typical file extension (including the leading .) registered for a given mime type: the reverse… |
Functions
detect_from_name()
mime.detect_from_name(name: string) -> string
Detects the mimetype of a file based on the extension defined in it’s path.
When name has more than one dot-separated suffix (archive. tar.gz),
the longest registered extension wins: .tar.gz if it’s registered,
falling back to .gz otherwise: rather than whichever single-suffix
entry happens to match first.
Example,
import mime
echo mime.detect_from_name('myimage.png')
Parameters
name(string)
Returns string
Note: Unrecognized extensions (including a name with no extension at all) return
'application/octet-stream'rather than raising.
detect_from_header()
mime.detect_from_header(file: file) -> string
Detects the mimetype of a file based on it’s file header.
When multiple file formats share very similar or shadowing file headers (such as the relationship between Zip files and Docx files), this method will perform an extension before returning it’s result.
Example,
import mime
var f = file('my_file.ext', 'rb')
echo mime.detect_from_header(f)
Parameters
file(file)
Returns string
Note: For dealing with files without extension, or where the accuracy of the file extension cannot be trusted, this method provides a more efficient lookup.
Note: This method may produce slightly more rigorous results
Note: This method requires that the file must be opened in binary mode.
detect()
mime.detect(file: file) -> string
Performs mimetype detection on a file.
this method is capable of detecting file mimetypes even in the absence of an extension.
If the file is opened in binary mode, it first attempt the more accurate header check. If the header check returns a generic result (i.e. application/octet-stream), it performs an extension lookup.
Example,
import mime
var f = file('myfile', 'rb')
# using 'rb' here for two reasons:
# 1. Our file has no extension, so extension based detection is impossible
# 2. We want more accuracy by having Mime check file headers
echo mime.detect(f)
Parameters
file(file)
Returns string
Note: this method gives the best result, but slightly slower than a direct lookup of name or header.
extend()
mime.extend(extension: string, format: instance, overwrite: ?bool) -> bool
Extends the mime module with support for files with the given extension as defined in the given format.
Example,
%> import mime
%> mime.detect_from_name('myfile.ppk')
'application/octet-stream'
%> mime.extend('.ppk', mime.MimeFormat('file/ppk'))
true
%> mime.detect_from_name('myfile.ppk')
'file/ppk'
Parameters
extension(string)format(MimeFormat)overwrite(?bool) — Defaultfalse. When an entry forextensionalready exists (including one of this module’s own built-in mappings) andoverwriteisfalse, the existing entry is left untouched and this returnsfalse; passtrueto replace it.
Returns bool
Note: the extension MUST start with
.
mime_to_extension()
mime.mime_to_extension(mimetype: string)
Looks up the typical file extension (including the leading .)
registered for a given mime type: the reverse of detect_from_name().
Example,
%> import mime
%> mime.mime_to_extension('image/png')
'.png'
Parameters
mimetype(string)
Returns — ?string: nil if no extension is registered for
mimetype.
Note: Several extensions can share one mime type (
.jpg/.jpeg,.htm/.html); whichever is registered first (in this module’s own declaration order for a built-in type, or whicheverextend()call added it for a custom one) is returned.
Classes
MimeFormat
class mime.MimeFormat
Mime format representation class.
Constructor
mime.MimeFormat(mimetype: string, header: ?list)
Parameters
mimetype(string)header(?list) — A list of one or more candidate magic-byte signatures, each itself a list of numbers (0-255) ornilfor a byte position to skip (a size field embedded in the middle of a signature, say: see the.webpentry in this module’s own table for an example).nilwhen name-only detection is all that’s available for this format.
Note: only the first 16 bytes of a file’s header will ever be used; a signature longer than that can never match.
O2021, Richard Ore
colors
import colors
This module provides functionalities for color conversion and manipulation.
This module also provides functionalities that enable cross-platform colored terminal outputs that will allow you create beautiful console apps that are user friendly.
RGB conversion to other colors that return a floating point or a list of
floating points do so to allow users get absolute precision since its
really easy for callers to do a num.round() on the components of the
resulting list.
Example
The example below uses this module to create a success message that will print correctly on almost all terminals (Only Windows 10 version 1901+ supported. All linux and OSX terminals are supported). Try it out!
import colors
colors.text('Successful!', colors.text_color.green)
The text() function can be nested. For example,
colors.text(colors.text('Successful!', colors.style.bold), colors.text_color.green)
The module also features multiple functions for color conversion. For example,
%> import colors
%> colors.rgb_to_cmyk(103, 13, 69)
[0, 87.37864077669903, 33.009708737864095, 59.6078431372549]
The terminal colors also have simple wrappers that allow supplied colors
to text() from various color formats. For example, we can specify the
color from the HTML hexadecimal color.
import colors
colors.text('Colored text!', colors.hex('#fc0'))
The colors API
Every public name in colors, wherever it is declared. Each links to
the page that documents it.
| Name | Kind | Summary |
|---|---|---|
colors.NAMED | constant | The CSS Color Level 4 named colors, mapped to their hexadecimal values. |
colors.ansi256_to_ansi | function | Converts ANSI-256 color number to ANSI-16 color number. |
colors.background | constant | Standard ANSI background colors available for console applications. |
colors.cmyk | function | Converts the given CMYK color to its terminal compatible color. |
colors.cmyk_to_rgb | function | Converts a CMYK color into its corresponding RGB color components. |
colors.contrast_ratio | function | Returns the WCAG 2.x contrast ratio between two colors, from 1 (no contrast) to 21 (black on white). |
colors.darken | function | Returns hex darkened by amount percentage points in HSL space (clamped to 0-100). |
colors.desaturate | function | Returns hex with its saturation decreased by amount percentage points in HSL space (clamped to 0-100). |
colors.grayscale | function | Returns hex fully desaturated (HSL saturation set to 0), the same hue and lightness otherwise preserved. |
colors.hex | function | Converts the given hexadecimal color to its terminal compatible color. |
colors.hex_to_ansi | function | Converts the given hexadecimal color to its ANSI-16 number. |
colors.hex_to_ansi256 | function | Converts the given hexadecimal color to its ANSI-256 number. |
colors.hex_to_rgb | function | Converts the hexadecimal string h to its RGBA component. |
colors.hsl | function | Converts the given HSL color to its terminal compatible color. |
colors.hsl_to_hsv | function | Converts a HSL color into its corresponding HSV color components. |
colors.hsl_to_rgb | function | Converts a HSL color into its corresponding RGB color components. |
colors.hsv | function | Converts the given HSV color to its terminal compatible color. |
colors.hsv_to_hsl | function | Converts a HSV color into its corresponding HSL color components. |
colors.hsv_to_rgb | function | Converts a HSV color into its corresponding RGB color components. |
colors.hwb | function | Converts the given HWB color to its terminal compatible color. |
colors.hwb_to_rgb | function | Converts a HWB color into its corresponding RGB color components. |
colors.invert | function | Returns hex with each RGB channel inverted (255 - channel). |
colors.is_hex | function | Returns true if color is a hexadecimal color that [[colors.hex_to_rgb]] would accept, and false… |
colors.is_named | function | Returns true if name is a CSS color name that [[colors.named]] would resolve, and false otherwise. |
colors.lab_to_rgb | function | Converts a LAB color into its corresponding RGB color components. |
colors.lighten | function | Returns hex lightened by amount percentage points in HSL space (clamped to 0-100). |
colors.mix | function | Linearly interpolates between two colors, including their alpha channels. |
colors.named | function | Returns the hexadecimal value of a CSS named color. |
colors.relative_luminance | function | Returns the relative luminance of an RGB color, from 0 (black) to 1 (white), as defined by WCAG 2.x. |
colors.rgb | function | Converts the given RGB color to its terminal compatible color. |
colors.rgb_to_ansi256 | function | Converts RGB color to ASI-256 color number. |
colors.rgb_to_cmyk | function | Converts a RGB color into its corresponding CMYK components. |
colors.rgb_to_hex | function | Converts a RGB components into its corresponding hexadecimal color. |
colors.rgb_to_hsl | function | Converts a RGB color into its corresponding HSL components. |
colors.rgb_to_hsv | function | Converts a RGB color into its corresponding HSV components. |
colors.rgb_to_hwb | function | Converts a RGB color into its corresponding HWB components. |
colors.rgb_to_lab | function | Converts a RGB color into its corresponding LAB color components. |
colors.rgb_to_xyz | function | Converts a RGB color into its corresponding XYZ color space components. |
colors.saturate | function | Returns hex with its saturation increased by amount percentage points in HSL space (clamped to 0-100). |
colors.style | constant | ANSI font styles available for console applications. |
colors.text | function | Returns a terminal printable text with the given color (or style) and background if given. |
colors.text_color | constant | Standard ANSI text colors available for console applications. |
colors.xyz | function | Converts the given XYZ color to its terminal compatible color. |
colors.xyz_to_rgb | function | Converts a XYZ color into its corresponding RGB color components. |
Constants
style
colors.style = {...}
ANSI font styles available for console applications.
text_color
colors.text_color = {...}
Standard ANSI text colors available for console applications.
background
colors.background = {...}
Standard ANSI background colors available for console applications.
NAMED
colors.NAMED: dict = {...}
The CSS Color Level 4 named colors, mapped to their hexadecimal values.
Values carry no leading #, matching every other function in this
module that produces a hexadecimal color. Both the American and British
spellings of the greys are present, as are rebeccapurple and
transparent, which is the only entry with an alpha component and
therefore the only 8-digit one.
Keys are lower case with no separators; use [[colors.named]] rather than indexing this directly if the name might arrive in mixed case or spaced out.
Functions
text()
colors.text(value, color, bg) -> string
Returns a terminal printable text with the given color (or style) and background if given.
Parameters
value(string)color(?int)bg(?int)
Returns string
Note: The color argument can be replace with a style.
rgb_to_ansi256()
colors.rgb_to_ansi256(r: int, g: int, b: int) -> number
Converts RGB color to ASI-256 color number.
Parameters
r(int)g(int)b(int)
Returns number
ansi256_to_ansi()
colors.ansi256_to_ansi(code: int) -> number
Converts ANSI-256 color number to ANSI-16 color number.
Parameters
code(int)
Returns number
is_hex()
colors.is_hex(color) -> bool
Returns true if color is a hexadecimal color that
[[colors.hex_to_rgb]] would accept, and false otherwise.
A leading # is optional and the digits may be in either case. This
raises nothing, so it is the way to test a color before converting it
rather than converting inside a catch.
%> import colors
%> colors.is_hex('#4f46e5')
true
%> colors.is_hex('rebeccapurple')
false
Parameters
color(any)
Returns bool
hex_to_rgb()
colors.hex_to_rgb(h: string) -> list
Converts the hexadecimal string h to its RGBA component.
Accepts an optional leading #, is case-insensitive, and accepts the
3/4/6/8-digit forms (#f0c, #f0c8, #ff00cc, #ff00ccbb). Following
the CSS Color 4 convention, alpha is the last component when present
(RRGGBBAA/RGBA, not AARRGGBB/ARGB): an omitted alpha means fully
opaque (1).
Parameters
h(string)
Returns list — [r, g, b, a]
Raises ValueError When h is not a valid hexadecimal color.
hex_to_ansi256()
colors.hex_to_ansi256(color: string) -> number
Converts the given hexadecimal color to its ANSI-256 number.
Parameters
color(string)
Returns number
hex_to_ansi()
colors.hex_to_ansi(color: string) -> number
Converts the given hexadecimal color to its ANSI-16 number.
Parameters
color(string)
Returns number
Note: For use with
text(), this should be preferred overhex_to_ansi256
hex()
colors.hex(color: string) -> number
Converts the given hexadecimal color to its terminal compatible color.
Parameters
color(string)
Returns number
Note: For use with
text(), this should be preferred overhex_to_ansi256andhex_to_ansi
Note: color can include the ‘#’ character. E.g.
#ff0.
rgb()
colors.rgb(r: int, g: int, b: int) -> number
Converts the given RGB color to its terminal compatible color.
Parameters
r(number)g(number)b(number)
Returns number
hsl()
colors.hsl(h: number, s: number, l: number) -> number
Converts the given HSL color to its terminal compatible color.
Parameters
h(number)s(number)l(number)
Returns number
hsv()
colors.hsv(h: number, s: number, v: number) -> number
Converts the given HSV color to its terminal compatible color.
Parameters
h(number)s(number)v(number)
Returns number
hwb()
colors.hwb(h: number, w: number, b: number) -> number
Converts the given HWB color to its terminal compatible color.
Parameters
h(number)w(number)b(number)
Returns number
cmyk()
colors.cmyk(c: number, m: number, y: number, k: number) -> number
Converts the given CMYK color to its terminal compatible color.
Parameters
c(number)m(number)y(number)k(number)
Returns number
xyz()
colors.xyz(x: number, y: number, z: number) -> number
Converts the given XYZ color to its terminal compatible color.
Parameters
x(number)y(number)z(number)
Returns number
rgb_to_hex()
colors.rgb_to_hex(r: int, g: int, b: int, a: ?int) -> string
Converts a RGB components into its corresponding hexadecimal color.
Following the CSS Color 4 convention, alpha (when given) is appended as
the last component (RRGGBBAA), and every channel is zero-padded to
two digits.
Parameters
r(int)g(int)b(int)a(?int)
Returns string
rgb_to_hsl()
colors.rgb_to_hsl(r: int, g: int, b: int) -> list[float]
Converts a RGB color into its corresponding HSL components.
Parameters
r(int)g(int)b(int)
Returns list[float]
rgb_to_hsv()
colors.rgb_to_hsv(r: int, g: int, b: int) -> list[float]
Converts a RGB color into its corresponding HSV components.
Parameters
r(int)g(int)b(int)
Returns list[float]
rgb_to_hwb()
colors.rgb_to_hwb(r: int, g: int, b: int) -> list[float]
Converts a RGB color into its corresponding HWB components.
Parameters
r(int)g(int)b(int)
Returns list[float]
rgb_to_cmyk()
colors.rgb_to_cmyk(r: int, g: int, b: int) -> list[float]
Converts a RGB color into its corresponding CMYK components.
Parameters
r(int)g(int)b(int)
Returns list[float]
rgb_to_xyz()
colors.rgb_to_xyz(r: int, g: int, b: int) -> list[float]
Converts a RGB color into its corresponding XYZ color space components.
Parameters
r(int)g(int)b(int)
Returns list[float]
rgb_to_lab()
colors.rgb_to_lab(r: int, g: int, b: int) -> list[float]
Converts a RGB color into its corresponding LAB color components.
Parameters
r(int)g(int)b(int)
Returns list[float]
hsl_to_rgb()
colors.hsl_to_rgb(h: number, s: number, l: number) -> list[float]
Converts a HSL color into its corresponding RGB color components.
Parameters
h(number)s(number)l(number)
Returns list[float]
hsl_to_hsv()
colors.hsl_to_hsv(h: number, s: number, l: number) -> list[float]
Converts a HSL color into its corresponding HSV color components.
Parameters
h(number)s(number)l(number)
Returns list[float]
hsv_to_rgb()
colors.hsv_to_rgb(h: number, s: number, v: number) -> list[float]
Converts a HSV color into its corresponding RGB color components.
Parameters
h(number)s(number)v(number)
Returns list[float]
hsv_to_hsl()
colors.hsv_to_hsl(h: number, s: number, v: number) -> list[float]
Converts a HSV color into its corresponding HSL color components.
Parameters
h(number)s(number)v(number)
Returns list[float]
hwb_to_rgb()
colors.hwb_to_rgb(h: number, w: number, b: number) -> list[float]
Converts a HWB color into its corresponding RGB color components.
Parameters
h(number)w(number)b(number)
Returns list[float]
cmyk_to_rgb()
colors.cmyk_to_rgb(c: number, m: number, y: number, k: number) -> list[float]
Converts a CMYK color into its corresponding RGB color components.
Parameters
c(number)m(number)y(number)k(number)
Returns list[float]
xyz_to_rgb()
colors.xyz_to_rgb(x: number, y: number, z: number) -> list[float]
Converts a XYZ color into its corresponding RGB color components.
Parameters
x(number)y(number)z(number)
Returns list[float]
lab_to_rgb()
colors.lab_to_rgb(l: number, a: number, b: number) -> list[float]
Converts a LAB color into its corresponding RGB color components. The
inverse of rgb_to_lab.
Parameters
l(number)a(number)b(number)
Returns list[float]
lighten()
colors.lighten(hex: string, amount: number) -> string
Returns hex lightened by amount percentage points in HSL space (clamped to 0-100).
Parameters
hex(string)amount(number)
Returns string
darken()
colors.darken(hex: string, amount: number) -> string
Returns hex darkened by amount percentage points in HSL space
(clamped to 0-100). The inverse of lighten.
Parameters
hex(string)amount(number)
Returns string
saturate()
colors.saturate(hex: string, amount: number) -> string
Returns hex with its saturation increased by amount percentage points in HSL space (clamped to 0-100).
Parameters
hex(string)amount(number)
Returns string
desaturate()
colors.desaturate(hex: string, amount: number) -> string
Returns hex with its saturation decreased by amount percentage
points in HSL space (clamped to 0-100). The inverse of saturate.
Parameters
hex(string)amount(number)
Returns string
grayscale()
colors.grayscale(hex) -> string
Returns hex fully desaturated (HSL saturation set to 0), the same hue and lightness otherwise preserved. This is an HSL desaturation, not a perceptual-luminance-weighted grayscale.
Parameters
hex(string)
Returns string
invert()
colors.invert(hex: string) -> string
Returns hex with each RGB channel inverted (255 - channel). Alpha,
if present, is left unchanged.
Parameters
hex(string)
Returns string
mix()
colors.mix(hex_a: string, hex_b: string, weight: ?number) -> string
Linearly interpolates between two colors, including their alpha channels.
Parameters
hex_a(string)hex_b(string)weight(?number) — How far from hex_a (0) to hex_b (1). Default: 0.5 (the midpoint).
Returns string
relative_luminance()
colors.relative_luminance(r, g, b) -> number
Returns the relative luminance of an RGB color, from 0 (black) to 1 (white), as defined by WCAG 2.x.
This is perceived brightness in linear light, not the l of HSL: pure
green is far brighter to the eye than pure blue even though HSL gives
both a lightness of 0.5. It is what contrast_ratio() is built on, and
what to compare against a threshold when deciding whether light or dark
text belongs on a background.
%> import colors
%> colors.relative_luminance(255, 255, 255)
1
%> colors.relative_luminance(0, 255, 0)
0.7152
Parameters
r(int)g(int)b(int)
Returns number
contrast_ratio()
colors.contrast_ratio(hex_a: string, hex_b: string) -> number
Returns the WCAG 2.x contrast ratio between two colors, from 1 (no contrast) to 21 (black on white).
A ratio of at least 4.5 meets WCAG AA for normal text (3 for large text); 7 meets AAA (4.5 for large text).
Parameters
hex_a(string)hex_b(string)
Returns number
named()
colors.named(name: string) -> string
Returns the hexadecimal value of a CSS named color.
Matching ignores case, whitespace, hyphens and underscores, so
'Tomato', 'tomato' and ' TOMATO ' all resolve, as do both
'darkseagreen' and 'Dark Sea Green'.
The result carries no leading #, the same as [[colors.rgb_to_hex]] and
[[colors.lighten]], so it can be handed straight to any function here
that takes a hexadecimal color. Every name returns 6 digits except
transparent, which returns the 8-digit '00000000'.
%> import colors
%> colors.named('rebeccapurple')
'663399'
%> colors.named('Dark Sea Green')
'8fbc8f'
Parameters
name(string)
Returns string
Raises ValueError When name is not a CSS color name.
Note: Spaces inside the name are ignored, so both the CSS spelling and the spaced-out reading of a name resolve to the same color.
is_named()
colors.is_named(name) -> bool
Returns true if name is a CSS color name that [[colors.named]] would
resolve, and false otherwise.
Parameters
name(string)
Returns bool
2022, Richard Ore and The Zuri Contributors
json
import json
Provides APIs for encoding and decoding JSON data.
JavaScript Object Notation (JSON) is a lightweight, text-based, language-independent data interchange format. It was derived from the ECMAScript Programming Language Standard. JSON defines a small set of formatting rules for the portable representation of structured data.
This implementation complies with RFC 8259.
JSON to Zuri value mapping
| JSON | Zuri |
|---|---|
| Null | Nil |
| String | String |
| Number | Number |
| Boolean | Boolean |
| Array | List |
| Object | Dict |
Zuri to JSON object mapping
| Zuri | JSON |
|---|---|
nil | Null |
| Integer | Number |
| Number | Number |
| Char | String |
| String | String |
| List | Array |
| Dict | Object |
Instance of class implementing to_json() decorator | Any |
Example,
%> import json
%> json.encode([1, 2, 3])
'[1,2,3]'
%>
%> json.encode({name: 'Zuri', version: '0.1.0'})
'{"name":"Zuri","version":"0.1.0"}'
%>
%> json.encode({name: 'Zuri', version: '0.1.0'}, false)
'{
"name": "Zuri",
"version": "0.1.0"
}'
The json API
Every public name in json, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
json.decode | function | Decodes the input JSON string into Zuri objects |
json.dump | function | Dumps the given value into a json file at the specified path. |
json.encode | function | JSON encodes the given value with a recursive depth up to max_depth. |
json.parse | function | Parses a file containing json data. |
Functions
encode()
json.encode(value, compact, max_depth) -> string
JSON encodes the given value with a recursive depth up to max_depth.
If compact is true, the resulting json string will be tightly
packed. i.e. spaces will be trimmed from objects and arrays. Otherwise,
the JSON output will be pretty formatted.
Parameters
value(any)compact(?bool) — Default value istrue.max_depth(?number) — is the maximum recursive depth for encoding, default = 1024.
Returns string
Raises Error: If the value cannot be encoded to json.
Note: pretty formatting use 2 spaces instead of tabs.
decode()
json.decode(value, allow_comments)
Decodes the input JSON string into Zuri objects
Parameters
value(string) — The string to decodeallow_comments(?bool) — Can be set to enable/disable C-style comments in json [default = true]
Returns — object
Raises Error: If the json is invalid.
parse()
json.parse(path, allow_comments)
Parses a file containing json data.
Parameters
path(string)allow_comments(?bool) — Can be set to enable/disable C-style comments in json [default = true]
Returns — object
Raises Error: If the file cannot be read or if the json is
invalid.
dump()
json.dump(value, path, compact, max_depth)
Dumps the given value into a json file at the specified path.
Parameters
value(any)path(string)compact(?bool) — Default value istrue.max_depth(?number) — is the maximum recursive depth for encoding, default = 1024.
Raises Error: If the file cannot be written to.
Note: pretty formatting use 2 spaces instead of tabs.
2021, Richard Ore and Zuri contributors
yaml
import yaml
A complete, YAML 1.2.2-compliant library for parsing and emitting YAML (YAML Ain’t Markup Language) documents.
This module implements the YAML 1.2.2 specification published at https://yaml.org/spec/1.2.2/ using the Core Schema for implicit type resolution, which is the schema used by most modern YAML implementations.
Supported features
Scalars
- Plain (unquoted) scalars with Core Schema type resolution -
Single-quoted scalars (no escape processing;
''→') - Double-quoted scalars with full escape sequence processing (\n \t \r \\ \" \/ \b \f \a \v \e \0 \xNN \uNNNN \UNNNNNNNN \N \_ \L \P) - Literal block scalars (
|) withclip,strip, andkeepchomping - Folded block scalars (
>) withclip,strip, andkeepchomping
Collections
- Block mappings (key: value pairs, indented) - Block
sequences (dash-prefixed lists, indented) - Flow mappings (
{ key: value, ... }) - Flow sequences (
[ item, item, ... ]) - Nested collections of any depth - Complex (non-scalar) mapping keys with?indicator
Document structure
- Multi-document streams separated by
---- Document end markers...-%YAMLdirectives (parsed; the major version must be1, and the version string must be well-formed, or parsing raises: the minor version isn’t otherwise checked) -%TAGdirectives (parsed and consumed; custom tag handles registered this way are not yet substituted into!handle!suffixtags) - UTF-8 input only, with BOM detection/stripping for that encoding. UTF-16/UTF-32 input is not supported.
Node properties
- Anchors (
&name) and aliases (*name) with full reference resolution - Merge keys (<<: *anchor) for dict merging - Explicit tags (!!str,!!int,!!float,!!bool,!!null,!!seq,!!map,!!binary,!<tag:yaml.org,2002:type>)
Core Schema type resolution (implicit)
null/Null/NULL/~/ empty →nil-true/True/TRUE/false/False/FALSE→ bool - Decimal integers (with optional+/-sign) → number - Octal integers0o[0-7]+→ number - Hexadecimal integers0x[0-9A-Fa-f]+→ number - Decimal floats (with exponent) → number -.inf/.Inf/.INF/±.inf→math.Infinity/-math.Infinity-.nan/.NaN/.NAN→0/0(NaN) - Everything else → string
Emitter features
- Round-trips scalars correctly (chooses minimal quoting) - Emits block or flow style per value type - Anchors and aliases for repeated object references - Configurable indentation (default: 2) - Configurable line width for scalar wrapping (default: 80)
Quick start
import yaml
# Parse a YAML string.
var data = yaml.parse('name: Alice\nage: 30\nscores: [98, 72, 85]')
echo data['name'] # Alice
echo data['age'] # 30
echo data['scores'][0] # 98
# Parse multiple documents.
var docs = yaml.parse_all('---\na: 1\n---\nb: 2\n')
echo docs.length() # 2
# Emit a Zuri value as YAML.
echo yaml.dump({ name: 'Bob', active: true, tags: ['x', 'y'] })
# name: Bob
# active: true
# tags: [x, y]
# Parse a YAML file.
var config = yaml.load_file('config.yaml')
# Emit to a file.
yaml.dump_file('out.yaml', data)
The yaml API
Every public name in yaml, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
yaml.YamlError | class | Error raised when a YAML parsing or emission error occurs. |
yaml.dump | function | Serialises a Zuri value to a YAML string. |
yaml.dump_all | function | Serialises a list of values as a multi-document YAML stream. |
yaml.dump_file | function | Serialises value to YAML and writes it to the file at path. |
yaml.load_file | function | Reads a YAML file from path and parses all documents in the stream. |
yaml.load_single_file | function | Reads a YAML file from path and parses the first document. |
yaml.parse | function | Parses a YAML string and returns the value of the first (or only) document. |
yaml.parse_all | function | Parses a YAML stream containing one or more documents and returns a list of all document values. |
Functions
parse()
yaml.parse(text: string) -> any
Parses a YAML string and returns the value of the first (or only) document.
Uses the Core Schema for implicit type resolution (see module description).
Examples
import yaml
# Scalar
yaml.parse('42') # 42 (number)
yaml.parse('hello') # 'hello' (string)
yaml.parse('true') # true (bool)
yaml.parse('~') # nil
# Mapping
yaml.parse('a: 1\nb: two') # { a: 1, b: 'two' }
# Sequence
yaml.parse('- x\n- y\n- z') # ['x', 'y', 'z']
# Flow styles
yaml.parse('{x: 1, y: [2, 3]}') # { x: 1, y: [2, 3] }
# Anchors and aliases
yaml.parse('a: &ref hello\nb: *ref') # { a: 'hello', b: 'hello' }
# Multi-line string
yaml.parse("msg: |\n hello\n world\n") # { msg: "hello\nworld\n" }
Parameters
text(string) — The YAML text to parse.
Returns any — The parsed Zuri value.
Raises YamlError On any YAML syntax or structural error.
parse_all()
yaml.parse_all(text: string) -> list
Parses a YAML stream containing one or more documents and returns a list of all document values.
Documents are separated by --- markers. A stream with a single
document and no --- marker returns a single-element list.
import yaml
var docs = yaml.parse_all("---\na: 1\n---\nb: 2\n")
echo docs.length() # 2
echo docs[0] # { a: 1 }
echo docs[1] # { b: 2 }
Parameters
text(string) — The YAML stream text.
Returns list — List of parsed document values (one per document).
Raises YamlError On any YAML syntax or structural error.
load_single_file()
yaml.load_single_file(path: string) -> any
Reads a YAML file from path and parses the first document.
import yaml
var config = yaml.load_file('config.yaml')
echo config['database']['host']
Parameters
path(string) — Filesystem path to the YAML file.
Returns any — The parsed value of the first document.
Raises YamlError On parse error.
Raises Error On file I/O error.
load_file()
yaml.load_file(path: string) -> list
Reads a YAML file from path and parses all documents in the stream.
Parameters
path(string) — Filesystem path to the YAML file.
Returns list — List of parsed document values.
Raises YamlError On parse error.
Raises Error On file I/O error.
dump()
yaml.dump(value, indent: ?int, width: ?int) -> string
Serialises a Zuri value to a YAML string.
The output is a valid YAML 1.2 document terminated by a newline. Collections use block style by default; small flat sequences may be emitted in flow style when they fit within the line width.
Examples
import yaml
yaml.dump(nil) # 'null\n'
yaml.dump(true) # 'true\n'
yaml.dump(42) # '42\n'
yaml.dump('hello world') # 'hello world\n'
yaml.dump('null') # "'null'\n" (quoted to avoid misparse)
yaml.dump({ name: 'Alice', age: 30 })
# name: Alice
# age: 30
yaml.dump(['a', 'b', 'c'])
# - a
# - b
# - c
# Nested structure.
yaml.dump({ servers: [{ host: 'a', port: 80 }, { host: 'b', port: 443 }] })
# servers:
# -
# host: a
# port: 80
# -
# host: b
# port: 443
Parameters
value(any) — The Zuri value to serialise.indent(?int) — Spaces per indentation level (default:2).width(?int) — Target line width for flow-style decisions (default:80).
Returns string — The YAML text (always ends with \n).
Raises YamlError On emission error (e.g. unserializable value).
dump_all()
yaml.dump_all(values: list, indent: ?int, width: ?int) -> string
Serialises a list of values as a multi-document YAML stream.
Each value is emitted as a separate YAML document, separated by ---.
import yaml
yaml.dump_all([{ a: 1 }, { b: 2 }])
# ---
# a: 1
# ---
# b: 2
Parameters
values(list) — List of Zuri values to serialise.indent(?int) — Spaces per indentation level (default:2).width(?int) — Target line width (default:80).
Returns string — The YAML stream text.
Raises YamlError On emission error.
dump_file()
yaml.dump_file(path: string, value, indent: ?int, width: ?int)
Serialises value to YAML and writes it to the file at path.
Creates or overwrites the file.
import yaml
yaml.dump_file('config.yaml', {
host: 'localhost',
port: 5432,
debug: false,
})
Parameters
path(string) — Filesystem path to write to.value(any) — The value to serialise.indent(?int) — Indentation level (default:2).width(?int) — Line width (default:80).
Raises YamlError On emission error.
Raises Error On file I/O error.
Classes
YamlError
class yaml.YamlError < Error
Error raised when a YAML parsing or emission error occurs.
Fields
| Field | Type | Description |
|---|---|---|
line | ||
column | ||
context |
Constructor
yaml.YamlError(message, line, column, context)
Parameters
message(string) — Error description.line(number) — Line number (1-based), ornil.column(number) — Column number (1-based), ornil.context(string) — Optional context (surrounding text), ornil.
2026, Richard Ore and The Zuri Contributors
toml
import toml
TOML, read for its values or edited in place without disturbing the file around them.
TOML is the format configuration files settle on when they have to be
edited by people and by programs both. This module answers both halves.
parse() and dump() treat a document as data, the way json and
yaml do. edit() treats it as a file somebody wrote, and keeps every
comment, blank line and quoting choice through a change.
The implementation follows TOML v1.0.0 in full.
import toml
var config = toml.parse('[server]\nhost = "localhost"\nport = 8080\n')
echo config['server']['port']
8080
What a value decodes to
| TOML | Zuri |
|---|---|
| string | string |
| integer | number, or bigint past 2^53 |
| float | number |
| boolean | bool |
| offset date-time | DateTime |
| local date-time | LocalDateTime |
| local date | LocalDate |
| local time | LocalTime |
| array | list |
| table | dict |
Zuri’s number is an IEEE-754 double, exact only to 2^53. TOML requires
the full signed 64-bit range, so an integer past what a double can carry
decodes to a bigint instead of rounding silently. An integer outside
the 64-bit range is a parse error, which is what TOML asks for.
Integers, floats, and the one thing that does not round-trip
The same double is behind 1 and 1.0, and nothing afterwards tells
them apart. Encoding has to guess, and it guesses integer whenever a
number has no fractional part. Wrap a number in Float to settle it:
import toml
echo toml.dump({ a: 1.0, b: toml.Float(1.0) })
a = 1
b = 1.0
This is the only place a value changes type across a parse() and
dump() pair. edit() is not affected: it never re-spells a value it
was not asked to change.
Editing a file in place
edit() returns a Document, which knows the text it came from.
Rendering one nobody touched reproduces the input byte for byte, and an
edit disturbs only the line it lands on:
import toml
var doc = toml.edit('# what we ship\n[package]\nname = "app"\nversion = "0.9.0" # bump me\n')
doc.set('package.version', '1.0.0')
doc.table('dependencies').set('http', '^2.0')
echo doc.to_string()
# what we ship
[package]
name = "app"
version = "1.0.0" # bump me
[dependencies]
http = "^2.0"
The comment stayed, the header stayed, and the trailing comment on the line that changed stayed with it.
The toml API
Every public name in toml, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
toml.Array | class | A TOML array, holding its items and the trivia between them. |
toml.ArrayOfTables | class | A [[header]] array of tables. |
toml.DateTime | class | A date and time with a UTC offset, TOML’s offset-date-time. |
toml.Document | class | A whole TOML document: every table in it, and every byte between them. |
toml.EditError | class | An edit that the document cannot accept. |
toml.EncodeError | class | A Zuri value that cannot be written as TOML. |
toml.Entry | class | One key = value pair. |
toml.Float | class | A number that must be written as a TOML float. |
toml.InlineTable | class | A TOML inline table, holding its entries and the trivia between them. |
toml.LocalDate | class | A calendar date with no time and no offset, TOML’s local-date. |
toml.LocalDateTime | class | A date and a wall-clock time with no offset, TOML’s local-date-time. |
toml.LocalTime | class | A wall-clock time with no date and no offset, TOML’s local-time. |
toml.ParseError | class | Text that is not valid TOML. |
toml.Table | class | A table: a [header] block, the root of a document, or a parent that only exists because something below it… |
toml.TomlError | class | Base class for every error this module raises. |
toml.Value | class | One value in a document: a scalar, an array, or an inline table. |
toml.document | function | Builds a Document from a plain Zuri value. |
toml.document.spell_basic_string | function | Returns text as a TOML basic string, quoted and escaped. |
toml.document.spell_float | function | Returns value as a TOML float, including the inf and nan spellings TOML defines. |
toml.document.spell_integer | function | Returns value as a TOML integer. |
toml.document.spell_key | function | Returns name as TOML would spell it as a key: bare when every character allows it, and quoted when not. |
toml.document.spell_multiline_string | function | Returns text as a TOML multi-line basic string. |
toml.document.spell_value | function | Builds the Value node for a plain Zuri value, choosing the TOML spelling as it goes. |
toml.dump | function | Returns value as TOML text. |
toml.dump_file | function | Writes value to the file at path as TOML, creating or overwriting it. |
toml.edit | function | Parses TOML text into a Document, which remembers how it was written. |
toml.edit_file | function | Reads the TOML file at path into a Document. |
toml.encoder.encode | function | Builds the document for a Zuri dictionary. |
toml.load_file | function | Reads the TOML file at path and returns it as a plain Zuri dictionary. |
toml.parse | function | Parses TOML text and returns it as a plain Zuri dictionary. |
toml.parser.parse | function | Reads TOML text and returns the document tree it describes. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
toml.document | toml.document.* | The document tree, which is the text. |
toml.encoder | import toml.encoder | Turning a plain Zuri value into a document. |
toml.errors | toml.errors.* | Every error the toml module raises, under one root. |
toml.parser | import toml.parser | Reading TOML text into the document tree, losing nothing. |
toml.values | toml.values.* | The TOML types that Zuri has no built-in equal for. |
Functions
parse()
toml.parse(text: string) -> dict
Parses TOML text and returns it as a plain Zuri dictionary.
A TOML document is always a table, so the result is always a dictionary, empty when the text held nothing but comments.
import toml
var config = toml.parse('title = "example"\n[owner]\nname = "Ada"\n')
echo config['title']
echo config['owner']['name']
example
Ada
Parameters
text(string) — The TOML text.
Returns dict
Raises ParseError On any syntax or structural error, carrying the
line and column it stopped at.
load_file()
toml.load_file(path: string) -> dict
Reads the TOML file at path and returns it as a plain Zuri dictionary.
import toml
var config = toml.load_file('zuri.toml')
echo config['package']['name']
Parameters
path(string) — Filesystem path to the TOML file.
Returns dict
Raises ParseError On any syntax or structural error.
Raises Error On any file error.
dump()
toml.dump(value, options: ?dict) -> string
Returns value as TOML text.
The result always ends in a line break. Keys keep the order the
dictionary holds them in unless sort_keys says otherwise.
import toml
echo toml.dump({
title: 'example',
owner: { name: 'Ada', active: true },
ports: [8000, 8001],
})
title = "example"
ports = [8000, 8001]
[owner]
name = "Ada"
active = true
Parameters
value(dict) — The value to write. A TOML document is a table, so this has to be a dictionary.options(?dict) —sort_keys(defaultfalse) writes keys in sorted order instead of insertion order.inline_threshold(default0, off) writes a flat table inline when its inline form is no longer than this many characters.multiline_strings(defaulttrue) writes a string holding a newline in triple quotes rather than escaping it.array_width(default80) is the length past which an array breaks onto one line per item.
Returns string
Raises EncodeError If value is not a dictionary, holds a nil
or a value with no TOML spelling, has a non-string key, or contains
itself.
dump_file()
toml.dump_file(path: string, value, options: ?dict)
Writes value to the file at path as TOML, creating or overwriting
it.
import toml
toml.dump_file('config.toml', {
server: { host: '0.0.0.0', port: 8080 },
})
Parameters
path(string) — Filesystem path to write to.value(dict) — The value to write.options(?dict) — The same optionsdump()takes.
Raises EncodeError If the value cannot be written.
Raises Error On any file error.
edit()
toml.edit(text: string) -> Document
Parses TOML text into a Document, which remembers how it was written.
This is the one to reach for when the file already exists and belongs to
somebody. A Document renders back byte for byte until it is changed,
and a change touches only what it names.
import toml
var doc = toml.edit('[a]\nx = 1 # keep me\ny = 2\n')
doc.set('a.x', 99)
doc.remove('a.y')
echo doc.to_string()
[a]
x = 99 # keep me
Parameters
text(string) — The TOML text.
Returns Document
Raises ParseError On any syntax or structural error.
edit_file()
toml.edit_file(path: string) -> Document
Reads the TOML file at path into a Document.
Document.save() writes it back.
import toml
var doc = toml.edit_file('zuri.toml')
doc.set('package.version', '2.0.0')
doc.save('zuri.toml')
Parameters
path(string) — Filesystem path to the TOML file.
Returns Document
Raises ParseError On any syntax or structural error.
Raises Error On any file error.
document()
toml.document(value, options: ?dict) -> Document
Builds a Document from a plain Zuri value.
dump() is this followed by to_string(). Reach for this one when the
document is going to be edited before it is written, or when a generated
file should be handed to the same code that handles a parsed one.
import toml
var doc = toml.document({ package: { name: 'app' } })
doc.set('package.version', '0.1.0')
echo doc.to_string()
[package]
name = "app"
version = "0.1.0"
Parameters
value(dict) — The value to write.options(?dict) — The same optionsdump()takes.
Returns Document
Raises EncodeError If the value cannot be written.
2026, Richard Ore and The Zuri Contributors
toml.document
import toml.document
tomllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledtoml.document.*needsimport toml.document.
The document tree, which is the text.
A TOML file read for its values can throw away everything that is not a
value. A TOML file read so that it can be written back cannot throw away
anything at all: the comment above a key, the blank line between two
tables, the choice of 0x1F over 31, the single quotes around a
Windows path. A program that rewrites a file it did not write has to
leave all of it alone.
So every node here holds two things: what it means, and the exact text
it was. Value keeps the decoded value beside the source spelling that
produced it. Entry keeps the whitespace on each side of its =.
Table keeps the trivia before its header. Rendering is concatenation,
and a document nobody edited comes back byte for byte.
Nodes are built by parser.zu when reading and by encoder.zu when
writing. Reach them through Document, which is what the module’s
public API hands out.
Functions
spell_key()
toml.document.spell_key(name: string) -> string
Returns name as TOML would spell it as a key: bare when every
character allows it, and quoted when not.
An empty key is legal in TOML and must be written "".
Parameters
name(string)
Returns string
spell_basic_string()
toml.document.spell_basic_string(text: string) -> string
Returns text as a TOML basic string, quoted and escaped.
Control characters that have no short escape are written as \uXXXX.
Every other character is passed through as itself, so the result stays
readable for anything that is not a control character.
Parameters
text(string)
Returns string
spell_multiline_string()
toml.document.spell_multiline_string(text: string) -> string
Returns text as a TOML multi-line basic string.
Newlines and single quotes pass through untouched; a run of three quotes and a trailing quote are escaped, since either would close the string early.
Parameters
text(string)
Returns string
spell_integer()
toml.document.spell_integer(value) -> string
Returns value as a TOML integer.
Parameters
value(number|bigint)
Returns string
spell_float()
toml.document.spell_float(value) -> string
Returns value as a TOML float, including the inf and nan spellings
TOML defines.
A whole number gains a .0, because a float that renders without one
reads back as an integer.
Parameters
value(number)
Returns string
spell_value()
toml.document.spell_value(value, path) -> Value
Builds the Value node for a plain Zuri value, choosing the TOML
spelling as it goes.
A string with a newline in it becomes a multi-line basic string; a whole
number becomes an integer; a Float becomes a float whatever its value.
A list becomes a single-line array and a dictionary becomes an inline
table, because this is the spelling a value takes when it is written in
place. Document.table() is how a value becomes a table block instead.
Parameters
value(any)path(?string) — Dotted path used in an error message.
Returns Value
Raises EncodeError If the value has no TOML spelling, or if a list
or dictionary contains itself.
Classes
Value
class toml.Value
One value in a document: a scalar, an array, or an inline table.
kind names the TOML type. value is the decoded Zuri value for a
scalar, or the Array or InlineTable node for a container. raw is
the exact source text of a scalar, which is what makes 0x1F survive a
round trip as 0x1F rather than as 31.
prefix and suffix hold the trivia on each side: whatever sat between
the = and the value, and whatever followed it on the same line, a
trailing comment included.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
kind | ||
value | ||
raw | ||
prefix | ||
suffix |
Constructor
toml.Value(kind, value, raw)
Parameters
kind(string) — One ofstring,integer,float,boolean,datetime,local-datetime,local-date,local-time,arrayorinline-table.value(any) — The decoded value, or the container node.raw(?string) — The exact source text, for a scalar.
Value.to_string()
toml.Value.to_string() -> string
Returns the value’s own text, without its surrounding trivia.
Returns string
Value.to_value()
toml.Value.to_value() -> any
Returns the value as a plain Zuri value, with every container below it converted too.
Returns any
Array
class toml.Array
A TOML array, holding its items and the trivia between them.
An array may run across lines and carry comments between its items, so each item keeps its own prefix and suffix and the array keeps whatever sits before the closing bracket.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
items | ||
trailing_comma | ||
trailing |
Constructor
toml.Array()
Array.to_string()
toml.Array.to_string() -> string
Returns the array as TOML text, brackets included.
Returns string
Array.to_value()
toml.Array.to_value() -> list
Returns the array as a plain Zuri list.
Returns list
InlineTable
class toml.InlineTable
A TOML inline table, holding its entries and the trivia between them.
An inline table is sealed once written: TOML forbids adding to one after the fact, and this module enforces that on edit as well as on parse.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
entries | ||
trailing |
Constructor
toml.InlineTable()
InlineTable.to_string()
toml.InlineTable.to_string() -> string
Returns the inline table as TOML text, braces included.
Returns string
InlineTable.to_value()
toml.InlineTable.to_value() -> dict
Returns the inline table as a plain Zuri dictionary, with dotted keys expanded into nested dictionaries.
Returns dict
Entry
class toml.Entry
One key = value pair.
path is the decoded key, which has more than one element when the key
was dotted. key_raw is the key exactly as written, dots, quotes and
interior spaces included, so a . b stays a . b.
prefix is everything from the end of the previous line to the first
character of the key, which is where a comment above the key lives.
newline is the line terminator, and is empty for an entry inside an
inline table.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
path | ||
key_raw | ||
prefix | ||
pre_eq | ||
value | ||
newline |
Constructor
toml.Entry(path, key_raw, value)
Parameters
path(list) — The decoded key path.key_raw(string) — The key exactly as written.value(Value) — The value this key holds.
Entry.to_string()
toml.Entry.to_string() -> string
Returns the whole pair as TOML text, its own trivia included.
Returns string
Entry.comment()
toml.Entry.comment() -> string|nil
Returns the comment written above this key, without its #, the single
space that usually follows it, or the indentation. Returns nil when
there is no comment.
Returns string|nil
Table
class toml.Table
A table: a [header] block, the root of a document, or a parent that
only exists because something below it was written.
entries holds the pairs written directly under this table’s header, in
document order. children maps a name to the Table or ArrayOfTables
below it.
A table is explicit when a header was written for it, and implicit
when it exists only because a deeper table named it. A table brought
into being by a dotted key is marked dotted: TOML allows sub-tables to
be added under one, but forbids reopening it with a header of its own.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
document | ||
path | ||
header_raw | ||
prefix | ||
suffix | ||
newline | ||
entries | ||
children | ||
explicit | ||
dotted | ||
is_aot_element |
Constructor
toml.Table(path)
Parameters
path(list) — The full key path from the root.
Table.to_string()
toml.Table.to_string() -> string
Returns this table’s own text: its header when it has one, then every pair written under it.
Child tables are not included. A document renders them in the order their headers appeared, which is not necessarily the order the tree nests them in.
Returns string
Table.to_value()
toml.Table.to_value() -> dict
Returns this table and everything under it as a plain Zuri dictionary.
Returns dict
Table.entry_at()
toml.Table.entry_at(path: list) -> Entry|nil
Returns the Entry whose key path is exactly path, or nil.
Parameters
path(list)
Returns Entry|nil
Table.keys()
toml.Table.keys() -> list
Returns the names of everything this table holds, pairs first and then child tables, in the order they were written.
A dotted key contributes only its first element, which is the name this table actually holds.
Returns list
Table.length()
toml.Table.length() -> number
Returns how many names this table holds.
Returns number
Table.get()
toml.Table.get(path) -> any
Returns the value stored at path, as a plain Zuri value, or nil when
nothing is there.
path is a dotted string or a list of key names. A dotted string splits
on ., so a key that contains a literal dot has to be passed as a list.
Parameters
path(string|list)
Returns any
Table.has()
toml.Table.has(path) -> bool
Returns true when something is stored at path.
Parameters
path(string|list)
Returns bool
Table.set()
toml.Table.set(path, value) -> Entry
Writes value at path, relative to this table.
A key that is already there keeps its line: only the value text changes,
so the spacing around the = and a trailing comment both survive. A key
that is not there is appended to this table, indented to match the pairs
already in it.
Parent tables along path are created as table blocks when they do not
exist. A dictionary passed as value is written as an inline table;
Document.table() is how a dictionary becomes a table block of its own
instead.
Parameters
path(string|list) — A dotted key path, or a list of names.value(any)
Returns Entry — The pair that was written.
Raises EditError If path runs through something that is not a
table, or names a table that already exists.
Raises EncodeError If value has no TOML spelling.
Table.remove()
toml.Table.remove(path) -> bool
Removes whatever sits at path, relative to this table.
A removed pair takes the comment written directly above it with it, because that comment described the pair. The blank lines around it are left alone.
Parameters
path(string|list)
Returns bool — True when something was there to remove.
Table.comment()
toml.Table.comment(key) -> string|nil
Returns the comment written above key, without its #, the single
space that usually follows it, or the indentation. Returns nil when
there is no comment.
Parameters
key(string|list)
Returns string|nil
Table.set_comment()
toml.Table.set_comment(key, text)
Writes text as the comment above key, replacing any comment already
there. Pass nil to remove it.
A text containing newlines becomes one # line per line.
Parameters
key(string|list)text(string|nil)
Raises EditError If key is not in this table.
ArrayOfTables
class toml.ArrayOfTables
A [[header]] array of tables.
Each element is a Table of its own, carrying its own header text and
its own trivia, because each was written separately.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
document | ||
path | ||
elements |
Constructor
toml.ArrayOfTables(path)
Parameters
path(list) — The full key path from the root.
ArrayOfTables.to_value()
toml.ArrayOfTables.to_value() -> list
Returns every element as a plain Zuri list of dictionaries.
Returns list
ArrayOfTables.length()
toml.ArrayOfTables.length() -> number
Returns how many elements the array holds.
Returns number
ArrayOfTables.to_string()
toml.ArrayOfTables.to_string()
ArrayOfTables.append()
toml.ArrayOfTables.append() -> Table
Adds a new [[header]] block to the end of the array and returns the
Table it made, which is where that element’s keys go.
Returns Table
ArrayOfTables.element()
toml.ArrayOfTables.element(index: number) -> Table
Returns the element at index, counting from zero.
Parameters
index(number)
Returns Table
Raises EditError If index is outside the array.
Document
class toml.Document
A whole TOML document: every table in it, and every byte between them.
A document that nobody has edited renders back exactly as it was read, byte for byte. An edited one keeps everything the edit did not touch, so a rewritten configuration file still reads as the file somebody wrote.
order is the sequence the table headers appeared in, which is not the
order the tree nests them in: a document may open [a], then [b],
then [a.c], and all three keep their places.
import toml
var doc = toml.edit('# the package\n[package]\nname = "app"\n')
doc.set('package.version', '1.0.0')
echo doc.to_string()
# the package
[package]
name = "app"
version = "1.0.0"
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
root | ||
order | ||
trailing | ||
bom | ||
newline |
Constructor
toml.Document()
Document.to_string()
toml.Document.to_string() -> string
Returns the whole document as TOML text.
Returns string
Document.to_value()
toml.Document.to_value() -> dict
Returns the document as a plain Zuri dictionary, with every table, array and scalar below it converted too.
Returns dict
Document.get()
toml.Document.get(path) -> any
Returns the value at path, or nil when nothing is there.
Parameters
path(string|list) — A dotted key path, or a list of key names for keys that contain a literal dot.
Returns any
Document.has()
toml.Document.has(path) -> bool
Returns true when something is stored at path.
Parameters
path(string|list)
Returns bool
Document.keys()
toml.Document.keys() -> list
Returns the top-level names the document holds, in the order they were written.
Returns list
Document.length()
toml.Document.length() -> number
Returns how many top-level names the document holds.
Returns number
Document.set()
toml.Document.set(path, value) -> Entry
Writes value at path.
A key that is already there keeps its line, its spacing and its trailing
comment; only the value text changes. A key that is not there is
appended to the table that should hold it, and the table blocks along
path are created when they do not exist.
import toml
var doc = toml.edit('[server]\nhost = "localhost" # bind address\n')
doc.set('server.host', '0.0.0.0')
doc.set('server.port', 8080)
echo doc.to_string()
[server]
host = "0.0.0.0" # bind address
port = 8080
Parameters
path(string|list) — A dotted key path, or a list of names for keys that contain a literal dot.value(any)
Returns Entry — The pair that was written.
Raises EditError If path runs through something that is not a
table, or names a table that already exists.
Raises EncodeError If value has no TOML spelling.
Document.remove()
toml.Document.remove(path) -> bool
Removes whatever sits at path, a pair or a whole table.
A removed pair takes the comment written directly above it with it. Removing a table removes everything under it.
Parameters
path(string|list)
Returns bool — True when something was there to remove.
Document.table()
toml.Document.table(path) -> Table
Returns the table at path, creating it as a table block when it is not
there.
This is how a dictionary becomes a [header] block rather than the
inline table set() would write.
import toml
var doc = toml.edit('name = "app"\n')
doc.table('dependencies').set('http', '^2.0')
echo doc.to_string()
name = "app"
[dependencies]
http = "^2.0"
Parameters
path(string|list)
Returns Table
Raises EditError If path runs through something that is not a
table.
Document.array_of_tables()
toml.Document.array_of_tables(path) -> ArrayOfTables
Returns the array of tables at path, creating an empty one when it is
not there.
append() on the result adds a [[header]] block and returns the
Table it made, which is where the new element’s keys go.
import toml
var doc = toml.edit('')
doc.array_of_tables('bin').append().set('name', 'serve')
echo doc.to_string()
[[bin]]
name = "serve"
Parameters
path(string|list)
Returns ArrayOfTables
Raises EditError If path names something that is not an array of
tables.
Document.save()
toml.Document.save(path: string)
Writes the document to path, creating or overwriting the file.
Parameters
path(string) — Filesystem path to write to.
Raises Error On any file error.
2026, Richard Ore and The Zuri Contributors
toml.encoder
import toml.encoder
tomldoes not re-export this module, so it is reached only by importing it directly.
Turning a plain Zuri value into a document.
The parser’s job is to lose nothing. This one’s is the opposite: there
is no original text to preserve, so every choice has to be made from
scratch. Which dictionaries become [table] blocks and which become
inline tables, which lists become [[array of table]] blocks, where a
long array breaks across lines, whether a string with a newline in it is
escaped or written out in triple quotes.
Every one of those is an option with a stated default, and the result is
a Document like any other, so a freshly encoded value and a parsed
file are the same kind of thing afterwards.
Functions
encode()
toml.encoder.encode(value, options) -> Document
Builds the document for a Zuri dictionary.
A TOML file is a table, so the value has to be a dictionary. Everything
under it is placed by what it is: a dictionary becomes a [table]
block, a non-empty list of dictionaries becomes a [[array of tables]],
and everything else becomes a pair on a line.
Parameters
value(dict) — The value to write.options(?dict) —sort_keys(defaultfalse) writes keys in sorted order instead of insertion order.inline_threshold(default0, off) writes a flat table inline when its inline form is no longer than this many characters.multiline_strings(defaulttrue) writes a string holding a newline in triple quotes rather than escaping it.array_width(default80) is the length past which an array breaks onto one line per item.
Returns Document
Raises EncodeError If the value is not a dictionary, holds a nil
or a value with no TOML spelling, has a non-string key, or contains
itself.
2026, Richard Ore and The Zuri Contributors
toml.errors
import toml.errors
tomllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledtoml.errors.*needsimport toml.errors.
Every error the toml module raises, under one root.
TomlError is that root. The three subclasses separate the cases a
caller handles differently: text that is not TOML, a Zuri value that has
no TOML spelling, and an edit that contradicts the document it was
applied to.
Each one carries the detail a message alone cannot. ParseError knows
the line and column it stopped at, EncodeError knows the type it could
not write, and EditError knows the key path it could not follow.
Classes
TomlError
class toml.TomlError < Error
Base class for every error this module raises.
Catch this to catch anything toml can do.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
toml.TomlError(message)
Parameters
message(string) — Human-readable description of the fault.
TomlError.to_string()
toml.TomlError.to_string()
ParseError
class toml.ParseError < TomlError
Text that is not valid TOML.
line and column are both 1-based and point at the character the
parser gave up on, which is where the fix goes. Both are -1 when the
fault belongs to the document as a whole rather than to one position in
it.
import toml
catch {
toml.parse('key = ')
} as error {
echo '${error.line}:${error.column} ${error.message}'
}
1:7 expected a value after '='
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
line | ||
column |
Constructor
toml.ParseError(message, line, column)
Parameters
message(string) — Human-readable description of the fault.line(?number) — 1-based line, or-1when not applicable.column(?number) — 1-based column, or-1when not applicable.
ParseError.to_string()
toml.ParseError.to_string()
EncodeError
class toml.EncodeError < TomlError
A Zuri value that cannot be written as TOML.
TOML has no null, so a nil anywhere in a value raises this rather than
being written as something that reads back differently. The same goes
for a function, a class, a cyclic structure, and a dictionary key that
is not a string.
path is the dotted key path the offending value sat at, so a rejected
leaf in a deep structure can be found without a search.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
path |
Constructor
toml.EncodeError(message, path)
Parameters
message(string) — Human-readable description of the fault.path(?string) — Dotted key path to the offending value.
EncodeError.to_string()
toml.EncodeError.to_string()
EditError
class toml.EditError < TomlError
An edit that the document cannot accept.
Raised when a key path runs through something that is not a table, such
as set('a.b', 1) on a document where a is already a string, and when
a path is malformed or empty.
path is the full dotted path as it was given, so the message names
what the caller asked for rather than the fragment that failed.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
path |
Constructor
toml.EditError(message, path)
Parameters
message(string) — Human-readable description of the fault.path(?string) — The dotted key path the edit was applied to.
EditError.to_string()
toml.EditError.to_string()
2026, Richard Ore and The Zuri Contributors
toml.parser
import toml.parser
tomldoes not re-export this module, so it is reached only by importing it directly.
Reading TOML text into the document tree, losing nothing.
The parser is a single pass over the characters. It builds the nodes in
document.zu, and every byte it consumes ends up in one of them: a
value’s text in its raw, the spaces around an = in the pair that
holds it, a comment in the prefix of whatever it sits above. Nothing is
normalised on the way in, so nothing has to be reconstructed on the way
out.
Alongside the tree it keeps a second, smaller structure recording what has been declared. TOML’s rules about redefinition are not about text at all: a table may be created implicitly by a header below it, created by a dotted key, or written out in full, and which of those happened decides whether the next header naming it is legal. Keeping that separate from the format tree is what keeps both readable.
Functions
parse()
toml.parser.parse(text: string) -> Document
Reads TOML text and returns the document tree it describes.
Every byte of the input ends up somewhere in the tree, so rendering the result without editing it reproduces the input exactly.
Parameters
text(string)
Returns Document
Raises ParseError On any syntax or structural error, with the line
and column it stopped at.
2026, Richard Ore and The Zuri Contributors
toml.values
import toml.values
tomllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledtoml.values.*needsimport toml.values.
The TOML types that Zuri has no built-in equal for.
TOML carries four temporal types and draws a line between an integer and
a float. Zuri has one number, which is an IEEE-754 double, and a
date.Date that always knows an offset. Neither gap can be closed by
picking a near-enough built-in: a local date that decoded as a Date
would grow a UTC offset it never had, and a float that decoded as a
number would be written back out as an integer.
So the four temporal types are their own classes here, and Float marks
a number that must stay a float. Each temporal class validates on
construction, renders itself in the exact form TOML specifies, and
answers equals() rather than ==, which compares instances by
identity.
import toml
var released = toml.LocalDate(2026, 9, 19)
echo released.to_string()
var stamp = toml.DateTime(2026, 9, 19, 13, 4, 5, 0, -300)
echo stamp.to_string()
2026-09-19
2026-09-19T13:04:05-05:00
Classes
LocalDate
class toml.LocalDate
A calendar date with no time and no offset, TOML’s local-date.
Spelled 1979-05-27. This is a date on the wall calendar: a birthday, a
release day, a contract term. It names no instant, so two LocalDates
from different timezones are comparable directly.
import toml
var released = toml.LocalDate(2026, 9, 19)
echo released.to_string()
echo released.year
echo released.equals(toml.LocalDate(2026, 9, 19))
2026-09-19
2026
true
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
toml.LocalDate(year, month, day)
Parameters
year(number) — From 0 to 9999.month(number) — From 1 to 12.day(number) — From 1 to the length of that month, leap years included.
Raises TomlError If any field is fractional or out of range, or if
the day does not exist in that month.
LocalDate.to_string()
toml.LocalDate.to_string() -> string
Returns the date as TOML spells it, YYYY-MM-DD.
Returns string
LocalDate.equals()
toml.LocalDate.equals(other) -> bool
Returns true when other is a LocalDate naming the same day. ==
makes the same comparison.
Parameters
other(any)
Returns bool
LocalDate.to_date()
toml.LocalDate.to_date() -> Date
Returns this date as a date.Date at midnight UTC.
The offset is an artefact of the conversion: a date.Date always has
one and a LocalDate never did. Use it for calendar arithmetic, not to
claim an instant the TOML file did not state.
Returns Date
LocalTime
class toml.LocalTime
A wall-clock time with no date and no offset, TOML’s local-time.
Spelled 07:32:00 or 07:32:00.999999. This is a time of day that
recurs: an opening hour, a cron-like schedule, a curfew.
Fractional seconds are kept to nanosecond precision and render with
trailing zeros removed, so half a second is .5 rather than
.500000000.
import toml
echo toml.LocalTime(7, 32, 0).to_string()
echo toml.LocalTime(7, 32, 0, 500000000).to_string()
07:32:00
07:32:00.5
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
toml.LocalTime(hour, minute, second, nanosecond)
Parameters
hour(number) — From 0 to 23.minute(number) — From 0 to 59.second(number) — From 0 to 60, the 60 admitting a leap second.nanosecond(?number) — From 0 to 999999999. Defaults to0.
Raises TomlError If any field is fractional or out of range.
LocalTime.to_string()
toml.LocalTime.to_string() -> string
Returns the time as TOML spells it, HH:MM:SS with a fractional part
only when there is one.
Returns string
LocalTime.equals()
toml.LocalTime.equals(other) -> bool
Returns true when other is a LocalTime naming the same instant of
the clock, fractional seconds included.
Parameters
other(any)
Returns bool
LocalDateTime
class toml.LocalDateTime
A date and a wall-clock time with no offset, TOML’s local-date-time.
Spelled 1979-05-27T07:32:00. It names a moment on somebody’s calendar
and clock without saying whose, which is what a log format or a schedule
written for one site means by a timestamp.
import toml
echo toml.LocalDateTime(1979, 5, 27, 7, 32, 0).to_string()
1979-05-27T07:32:00
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
toml.LocalDateTime(year, month, day, hour, minute, second, nanosecond)
Parameters
year(number) — From 0 to 9999.month(number) — From 1 to 12.day(number) — From 1 to the length of that month.hour(number) — From 0 to 23.minute(number) — From 0 to 59.second(number) — From 0 to 60.nanosecond(?number) — From 0 to 999999999. Defaults to0.
Raises TomlError If any field is fractional or out of range.
LocalDateTime.to_string()
toml.LocalDateTime.to_string() -> string
Returns the timestamp as TOML spells it, the date and the time joined by
T.
Returns string
LocalDateTime.equals()
toml.LocalDateTime.equals(other) -> bool
Returns true when other is a LocalDateTime naming the same date and
time.
Parameters
other(any)
Returns bool
LocalDateTime.to_date()
toml.LocalDateTime.to_date() -> Date
Returns this timestamp as a date.Date read as UTC.
The offset is an artefact of the conversion. A LocalDateTime
deliberately does not know one, so reading the result as an instant
asserts something the TOML file did not.
Returns Date
DateTime
class toml.DateTime
A date and time with a UTC offset, TOML’s offset-date-time.
Spelled 1979-05-27T07:32:00Z or 1979-05-27T00:32:00-07:00. This is
the only one of the four that names an unambiguous instant, and the only
one that can be compared across sites.
The offset is held in minutes, signed, so -300 is -05:00. A zero
offset renders as Z; a document that spelled it +00:00 keeps that
spelling through an edit, because the raw text is preserved alongside
the value.
import toml
echo toml.DateTime(1979, 5, 27, 7, 32, 0, 0, 0).to_string()
echo toml.DateTime(1979, 5, 27, 0, 32, 0, 0, -420).to_string()
1979-05-27T07:32:00Z
1979-05-27T00:32:00-07:00
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
toml.DateTime(year, month, day, hour, minute, second, nanosecond, offset_minutes)
Parameters
year(number) — From 0 to 9999.month(number) — From 1 to 12.day(number) — From 1 to the length of that month.hour(number) — From 0 to 23.minute(number) — From 0 to 59.second(number) — From 0 to 60.nanosecond(?number) — From 0 to 999999999. Defaults to0.offset_minutes(?number) — Minutes east of UTC, from-1439to1439. Defaults to0, which renders asZ.
Raises TomlError If any field is fractional or out of range.
DateTime.to_string()
toml.DateTime.to_string() -> string
Returns the timestamp as TOML spells it, with Z for a zero offset and
+HH:MM or -HH:MM otherwise.
Returns string
DateTime.equals()
toml.DateTime.equals(other) -> bool
Returns true when other is a DateTime with the same fields and the
same offset.
Two timestamps naming the same instant through different offsets are not
equal here. Compare to_date().to_time() for that.
Parameters
other(any)
Returns bool
DateTime.to_date()
toml.DateTime.to_date() -> Date
Returns this timestamp as a date.Date carrying the same offset.
This is the one temporal type that converts without inventing anything: both sides name the same instant.
Returns Date
Float
class toml.Float
A number that must be written as a TOML float.
Zuri’s number is an IEEE-754 double, so 1.0 and 1 are one value
and nothing distinguishes them afterwards. TOML does distinguish them,
and an encoder with only the value to go on has to guess: it writes an
integer whenever the number has no fractional part, which turns a 1.0
into a 1.
Wrap the number to settle it. Float means float, whatever the value
happens to be:
import toml
echo toml.dump({ ratio: 1.0 })
echo toml.dump({ ratio: toml.Float(1.0) })
ratio = 1
ratio = 1.0
Decoding never produces one: a TOML float decodes to a plain number,
because a wrapper on every float would make every read pay for a
distinction most programs do not use.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
toml.Float(value)
Parameters
value(number) — The number to write as a float.inf,-infandnanare all accepted; TOML spells theminf,-infandnan.
Raises TomlError If value is not a number.
Float.to_string()
toml.Float.to_string() -> string
Returns the wrapped number as TOML spells it.
Returns string
Float.equals()
toml.Float.equals(other) -> bool
Returns true when other is a Float wrapping the same number.
nan never equals anything, this included, which matches what the bare
comparison does.
Parameters
other(any)
Returns bool
2026, Richard Ore and The Zuri Contributors
csv
import csv
A complete, RFC 4180-compliant library for reading and writing Comma-Separated Values (CSV) data.
This module implements the format defined in RFC 4180: “Common Format and MIME Type for Comma-Separated Values (CSV) Files”: and extends it with the practical accommodations required by real-world CSV data, including configurable delimiters, optional quoting strategies, BOM detection, and lenient parsing of files that deviate from the strict spec.
Key CSV rules
- Records are separated by CRLF (
\r\n). The last record may omit the trailing line break. - Fields containing the delimiter, a double-quote, CR, or LF must be enclosed in double-quotes. - A double-quote inside a quoted field is escaped by doubling it (""). - An optional header record may appear as the first line. - All records must contain the same number of fields.
Extensions beyond RFC 4180
- Configurable delimiter: any single character (tab for
TSV, pipe, etc.) - Configurable quote character: defaults to
". - LF-only and CR-only line endings: accepted on input; CRLF on output. - BOM stripping: UTF-8 BOM (
\xEF\xBB\xBF) is silently removed. - Empty last line: a trailing newline after the last record is ignored. - Quoting strategies:QUOTE_MINIMAL(default),QUOTE_ALL,QUOTE_NONNUMERIC,QUOTE_NONE. - Lenient mode: tolerate unquoted fields containing the delimiter and other minor deviations common in real-world CSV files.
Quick start
Reading
import csv
# Parse a CSV string directly.
var table = csv.parse('name,age\r\nAlice,30\r\nBob,25')
echo table[0] # { name: 'Alice', age: '30' }
echo table[1] # { name: 'Bob', age: '25' }
# Stream a file row by row.
var reader = csv.Reader(file('data.csv'))
for row in reader {
echo row
}
reader.close()
Writing
import csv
# Encode a list of dicts to a CSV string.
var data = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
]
echo csv.stringify(data)
# name,age\r\nAlice,30\r\nBob,25\r\n
# Stream rows to a file.
var writer = csv.Writer(file('out.csv', 'w'))
writer.write_header(['name', 'age'])
writer.write_row(['Alice', 30])
writer.write_row(['Bob', 25])
writer.close()
The csv API
Every public name in csv, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
csv.CsvError | class | Error raised when a CSV parsing or encoding error is encountered. |
csv.Dialect | class | Encapsulates the formatting parameters that govern how CSV data is read or written. |
csv.QUOTE_ALL | constant | Quote every field unconditionally, regardless of content. |
csv.QUOTE_MINIMAL | constant | Quote only fields that contain the delimiter, the quote character, or a line break. |
csv.QUOTE_NONE | constant | Never quote fields. |
csv.QUOTE_NONNUMERIC | constant | Quote all non-numeric fields. |
csv.Reader | class | An incremental, row-by-row CSV reader that wraps a Zuri file object (or any object implementing .read()… |
csv.Writer | class | An incremental, row-by-row CSV writer that wraps a Zuri file object (or any object implementing .write()… |
csv.format_record | function | Encodes a single list of values as one CSV record and returns the string (including the line terminator). |
csv.parse | function | Parses a CSV string and returns all records as a list. |
csv.parse_record | function | Parses a single CSV record string and returns the fields as a list. |
csv.read_file | function | Reads a CSV file and returns all records as a list. |
csv.sniff_dialect | function | Attempts to detect the field delimiter used in sample and returns a Dialect configured with it. |
csv.stringify | function | Encodes a list of records into a CSV string and returns it. |
csv.write_file | function | Encodes a list of records and writes them to a CSV file, creating or overwriting the file at path. |
Constants
QUOTE_MINIMAL
csv.QUOTE_MINIMAL = 0
Quote only fields that contain the delimiter, the quote character, or a line break. This is the default and produces the most compact output. Compliant with RFC 4180 §2 rule 6.
QUOTE_ALL
csv.QUOTE_ALL = 1
Quote every field unconditionally, regardless of content. Produces the most portable output; useful when the consumer is known to require quoted fields.
QUOTE_NONNUMERIC
csv.QUOTE_NONNUMERIC = 2
Quote all non-numeric fields. Fields whose string representation is a valid integer or floating-point number are written unquoted.
QUOTE_NONE
csv.QUOTE_NONE = 3
Never quote fields. If a field contains the delimiter or a line break, a
CsvError is raised (in strict mode) or the character is written as-is
(in lenient mode). Use only when you are certain the data contains no
special characters.
Functions
sniff_dialect()
csv.sniff_dialect(sample, candidate_delimiters) -> Dialect
Attempts to detect the field delimiter used in sample and returns a
Dialect configured with it.
Each candidate delimiter is scored by how consistently it occurs across
the sample’s lines: a real delimiter tends to appear the same number of
times on every line, while one that just happens to show up in the data
(a comma inside a sentence, say) doesn’t. The best-scoring candidate
wins; if none of them appear consistently anywhere in sample, the
first candidate (, by default) is used.
This only detects the delimiter: not the quote character, whether a
header row is present, or any other Dialect field. Set those on the
returned Dialect yourself if they differ from the defaults.
Example,
%> var dialect = csv.sniff_dialect("a;b;c\n1;2;3\n4;5;6")
%> dialect.delimiter
';'
Parameters
sample(string) — Doesn’t need to be the whole file: a few representative lines are enough.candidate_delimiters(?list) — Default[',', '\t', ';', '|'].
Returns Dialect
parse()
csv.parse(text, dialect) -> list
Parses a CSV string and returns all records as a list.
When dialect.has_header is true (or has_header is passed as
true), the first record is treated as a header and every subsequent
record is returned as a dict keyed by column name. Otherwise, records
are returned as lists of strings.
This function is the simplest way to parse a complete CSV document that already resides in memory.
Examples
import csv
# No header: returns list of lists.
var rows = csv.parse('a,b,c\r\n1,2,3\r\n4,5,6')
echo rows[0] # ['a', 'b', 'c']
echo rows[1] # ['1', '2', '3']
# With header: returns list of dicts.
var d = csv.Dialect()
d.has_header = true
rows = csv.parse('name,age\r\nAlice,30\r\nBob,25', d)
echo rows[0] # { name: 'Alice', age: '30' }
echo rows[1] # { name: 'Bob', age: '25' }
# Tab-separated, no header.
var td = csv.Dialect()
td.delimiter = '\t'
rows = csv.parse('x\t1\ny\t2', td)
echo rows[0] # ['x', '1']
Parameters
text(string) — The CSV text to parse.dialect(?Dialect) — Optional dialect (defaults toDialect()).
Returns list — List of lists or dicts depending on has_header.
Raises CsvError On parse error in strict mode.
stringify()
csv.stringify(rows, dialect, keys) -> string
Encodes a list of records into a CSV string and returns it.
Each element of rows may be either a list (field values in order) or a
dict. When rows contains dicts, keys controls the column order; if
omitted, the keys of the first dict determine the order. A header row is
written automatically when rows contains dicts.
Examples
import csv
# From lists: no header.
echo csv.stringify([['Alice', 30], ['Bob', 25]])
# Alice,30\r\nBob,25\r\n
# From dicts: header inferred.
echo csv.stringify([
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
])
# name,age\r\nAlice,30\r\nBob,25\r\n
# Custom dialect.
var d = csv.Dialect()
d.quoting = csv.QUOTE_ALL
echo csv.stringify([['hello, world', 42]], d)
# "hello, world","42"\r\n
Parameters
rows(list) — List of lists or dicts to encode.dialect(?Dialect) — Optional dialect (defaults toDialect()).keys(?list) — Optional column key order whenrowsare dicts.
Returns string — The encoded CSV text.
Raises CsvError On encoding error.
parse_record()
csv.parse_record(line, dialect) -> list
Parses a single CSV record string and returns the fields as a list.
This is a lightweight utility for parsing a single line when you already have it isolated (e.g. from a line-buffered reader). No record separator detection is performed.
import csv
csv.parse_record('"hello, world",42,true')
# ['hello, world', '42', 'true']
csv.parse_record('"He said ""hi""",ok')
# ['He said "hi"', 'ok']
Parameters
line(string) — A single CSV record (with or without line ending).dialect(?Dialect) — Optional dialect.
Returns list — The parsed field values.
Raises CsvError On parse error.
format_record()
csv.format_record(row, dialect) -> string
Encodes a single list of values as one CSV record and returns the string (including the line terminator).
import csv
csv.format_record(['Alice', 'New York, NY', 'She said "hello"'])
# 'Alice,"New York, NY","She said ""hello"""\r\n'
Parameters
row(list) — Field values to encode.dialect(?Dialect) — Optional dialect.
Returns string — The encoded record with line terminator.
Raises CsvError On encoding error.
read_file()
csv.read_file(path, dialect) -> list
Reads a CSV file and returns all records as a list.
Convenience wrapper around Reader that opens, reads, and closes in one
call. Use Reader directly when you need streaming or finer control.
import csv
var d = csv.Dialect()
d.has_header = true
var rows = csv.read_file('employees.csv', d)
echo rows[0]['name']
Parameters
path(string) — Filesystem path to the CSV file.dialect(?Dialect) — Optional dialect.
Returns list — All records as lists or dicts.
Raises CsvError On parse error.
Raises Error On file I/O error.
write_file()
csv.write_file(path, rows, dialect, keys)
Encodes a list of records and writes them to a CSV file, creating or
overwriting the file at path.
Convenience wrapper around Writer. Use Writer directly when you need
streaming or finer control.
import csv
csv.write_file('out.csv', [
{ name: 'Alice', score: 98 },
{ name: 'Bob', score: 72 },
])
Parameters
path(string) — Filesystem path to write to.rows(list) — List of lists or dicts.dialect(?Dialect) — Optional dialect.keys(?list) — Optional column key order for dict rows.
Raises CsvError On encoding error.
Raises Error On file I/O error.
Classes
CsvError
class csv.CsvError < Error
Error raised when a CSV parsing or encoding error is encountered.
Fields
| Field | Type | Description |
|---|---|---|
line | ||
column |
Constructor
csv.CsvError(message, line, column)
Parameters
message(string) — Error description.line(number) — Line number (1-based).column(number) — Column number (1-based).
Dialect
class csv.Dialect
Encapsulates the formatting parameters that govern how CSV data is read
or written. Pass a Dialect instance to Reader or Writer to
override the defaults.
Example: tab-separated values (TSV)
import csv
var tsv = csv.Dialect()
tsv.delimiter = '\t'
var reader = csv.Reader(file, tsv)
Example: pipe-delimited, quote-all
var d = csv.Dialect()
d.delimiter = '|'
d.quoting = csv.QUOTE_ALL
var writer = csv.Writer(file, d)
Fields
| Field | Type | Description |
|---|---|---|
delimiter | string | The single-character field delimiter. |
quote_char | string | The character used to quote fields. |
line_terminator | string | The line terminator written between records. |
quoting | number | Quoting strategy. |
trim | bool | When true (default), leading and trailing whitespace is stripped from unquoted fields during reading. |
lenient | bool | When true, the parser accepts minor deviations from RFC 4180 such as a quote character that appears in the… |
has_header | bool | When true, reading expects the first record to be a header row and returns subsequent records as dicts… |
max_field_size | number | Maximum number of characters allowed in a single field. |
Dialect.validate()
csv.Dialect.validate()
Validates that all dialect fields hold consistent, legal values.
Raises CsvError If any field is invalid.
Reader
class csv.Reader
An incremental, row-by-row CSV reader that wraps a Zuri file object
(or any object implementing .read() and .close()).
Reader implements the iterator protocol so it can be used directly in
a for loop.
Example: reading a file with a header row
import csv
import io
var d = csv.Dialect()
d.has_header = true
var reader = csv.Reader(file('employees.csv'), d)
for row in reader {
echo row['name'] + ' earns ' + row['salary']
}
reader.close()
Example: tab-separated file, no header
import csv
import io
var d = csv.Dialect()
d.delimiter = '\t'
var reader = csv.Reader(file('data.tsv'), d)
for row in reader {
echo row # list: ['field1', 'field2', ...]
}
reader.close()
Notes
- The file is read all at once into memory. For very large
files (> a few hundred MB), consider streaming line by line using
read_record(). - Whendialect.has_headeristrue, the first record is consumed as the header and is accessible via theheadersproperty. - When
dialect.has_headeristrueand a data row has fewer fields than the header, the missing fields are set tonil. - Whendialect.has_headeristrueand a row has more fields than the header, the extra fields are silently discarded (lenient mode) or raiseCsvError(strict mode).
Fields
| Field | Type | Description |
|---|---|---|
dialect | Dialect | The dialect governing parsing behaviour. |
headers | list | The header row, as a list of strings, once the first record has been consumed. |
record_num | number | The 1-based index of the most recently returned record (not counting the header row). |
Constructor
csv.Reader(file, dialect)
Creates a Reader from a file object.
Parameters
file(file) — An open, readable Zuri file object.dialect(?Dialect) — Optional dialect (defaults toDialect()).
Raises CsvError If dialect validation fails.
Reader.read_record()
csv.Reader.read_record() -> list
Reads and returns the next record, or nil when all records have been
consumed.
When dialect.has_header is true, returns a dict keyed by header
name. Otherwise returns a list of field strings.
Returns list — | dict | nil
Raises CsvError On parse error in strict mode.
Reader.read_all()
csv.Reader.read_all() -> list
Reads all remaining records into a list and returns it.
Returns list — A list of lists (or dicts when dialect.has_header
is true).
Reader.close()
csv.Reader.close()
Closes the underlying file object. Always call close() when done, even
if an error occurred.
Writer
class csv.Writer
An incremental, row-by-row CSV writer that wraps a Zuri file object
(or any object implementing .write() and .close()).
Example: writing dicts (with header)
import csv
import io
var writer = csv.Writer(file('out.csv', 'w'))
writer.write_header(['id', 'name', 'score'])
writer.write_row([1, 'Alice', 98.5])
writer.write_row([2, 'Bob', 72.0])
writer.close()
Example: QUOTE_ALL with tab delimiter
import csv
import io
var d = csv.Dialect()
d.delimiter = '\t'
d.quoting = csv.QUOTE_ALL
var writer = csv.Writer(file('out.tsv', 'w'), d)
writer.write_row(['Alice', 'Engineer', 'New York'])
writer.close()
Notes
write_row()accepts a list of any values; each is converted to a string with.to_string()before encoding. -write_dict()accepts a dict and akeyslist that controls the field order and which keys are written. - The writer does not buffer: every call towrite_row()immediately writes to the underlying file.
Fields
| Field | Type | Description |
|---|---|---|
dialect | Dialect | The dialect governing encoding behaviour. |
record_num | number | The number of records written so far (not counting the header row). |
Constructor
csv.Writer(file, dialect)
Creates a Writer targeting a file object.
Parameters
file(file) — An open, writable Zuri file object.dialect(?Dialect) — Optional dialect (defaults toDialect()).
Raises CsvError If dialect validation fails.
Writer.write_row()
csv.Writer.write_row(row)
Encodes and writes a single record from a list of values.
Each element of row is converted to a string via .to_string() and
then encoded according to the dialect’s quoting strategy.
Parameters
row(list) — The field values for this record.
Raises CsvError On encoding error (e.g. QUOTE_NONE with special
chars).
Writer.write_header()
csv.Writer.write_header(headers)
Encodes and writes a header record from a list of column name strings.
Functionally identical to write_row() but does not increment
record_num, making it semantically clear that this is metadata.
Parameters
headers(list) — The column names.
Raises CsvError On encoding error.
Writer.write_dict()
csv.Writer.write_dict(row, keys)
Encodes and writes a single record from a dict, using keys to
determine field order. Missing keys produce empty fields.
writer.write_dict({ name: 'Alice', age: 30 }, ['name', 'age'])
Parameters
row(dict) — The record as a key-value dict.keys(list) — Ordered list of keys to extract fromrow.
Raises CsvError On encoding error.
Writer.write_dicts()
csv.Writer.write_dicts(rows, keys)
Writes a list of records from a list of dicts, preceded by a header row whose columns are derived from the keys of the first record.
If keys is provided it controls column order; otherwise the keys of
the first row are used in their natural iteration order.
writer.write_dicts([
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
])
Parameters
rows(list) — List of dicts, each representing one record.keys(?list) — Optional ordered key list. Inferred from the first row when omitted.
Raises CsvError On encoding error.
Writer.close()
csv.Writer.close()
Closes the underlying file object. Always call close() when done, even
if an error occurred.
2026, Richard Ore and The Zuri Contributors
base64
import base64
This module provides interface for encoding binary data into strings and decoding such encoded strings back into binary data based on the base64 encoding specified in RFC4648
The base64 API
Every public name in base64, wherever it is declared. Each links to
the page that documents it.
| Name | Kind | Summary |
|---|---|---|
base64.decode | function | Decodes a base64 string into it’s corresponding bytes. |
base64.encode | function | Encodes a byte array into a base64 string |
Functions
encode()
base64.encode(data: bytes) -> string
Encodes a byte array into a base64 string
Parameters
data(bytes)
Returns string
decode()
base64.decode(data: string) -> bytes
Decodes a base64 string into it’s corresponding bytes.
Parameters
data(string)
Returns bytes
2021, Richard Ore and Zuri contributors
struct
import struct
This module provides functions for converting between Zuri values and C/C++/Rust structs and vice-versa in the binary format.
The pack and unpack functions behave similarly as the pack and
unpack functions from Perl and PHP (more similar to the PHP version)
with few major extensions to their format language.
Format language
A format string is a sequence of /-separated segments. Each segment is
one or more CODE[COUNT] groups, optionally followed by :NAME to
label the field(s) that group produces when unpacking:
"Nsize:len/A16:name/C4"
CODEis one of the format characters in the table below. -COUNTis a decimal integer, or*to mean “the rest” (the whole remaining argument for a string-like code, or every remaining argument/byte for a numeric code). Omitted means1.:NAMEnames the field(s) produced by that ONE group when unpacking. A count > 1 numbers the keysNAME1,NAME2, … A segment carrying a:NAMEmay only contain a single group. - A group with no:NAMEgets a purely numeric key that runs once, globally, for the whole format string; it never resets and so never silently overwrites an earlier unnamed field.
Format codes
Code | Bytes | Meaning —–|—––|–––– a | count | NUL-padded
string A | count | SPACE-padded string (trailing NUL/space trimmed on
unpack) Z | count | NUL-padded, NUL-terminated string (C-string
semantics) h | ceil(count/2) | Hex string, low nibble first H |
ceil(count/2) | Hex string, high nibble first c | 1 | signed 8-bit
integer C | 1 | unsigned 8-bit integer ? | 1 | boolean s | 2 |
signed 16-bit integer, native byte order S | 2 | unsigned 16-bit
integer, native byte order n | 2 | unsigned 16-bit integer, big-endian
v | 2 | unsigned 16-bit integer, little-endian i | 4 | signed 32-bit
integer, native byte order I | 4 | unsigned 32-bit integer, native
byte order l | 4 | signed 32-bit integer, native byte order L | 4 |
unsigned 32-bit integer, native byte order N | 4 | unsigned 32-bit
integer, big-endian V | 4 | unsigned 32-bit integer, little-endian q
| 8 | signed 64-bit integer, native byte order Q | 8 | unsigned 64-bit
integer, native byte order J | 8 | unsigned 64-bit integer, big-endian
P | 8 | unsigned 64-bit integer, little-endian u | 16 | signed
128-bit integer, little-endian U | 16 | unsigned 128-bit integer,
little-endian f | 4 | float, native byte order g | 4 | float,
little-endian G | 4 | float, big-endian d | 8 | double, native byte
order e | 8 | double, little-endian E | 8 | double, big-endian w |
2 | IEEE-754 half-precision float, little-endian W | 2 | IEEE-754
half-precision float, big-endian x | count | NUL byte(s); consumes no
argument X | count | back up count byte(s) Z |; | (see above) @
|; | seek/pad to absolute position count
Integer precision
Zuri numbers are IEEE-754 doubles, which can only represent integers
exactly up to 2^53. Every integer-producing code here (q/Q/J/P,
and the new u/U) automatically promotes its result to a bigint
Value instead of a number whenever the unpacked value falls outside
that safe range, rather than silently losing precision. Packing accepts
either a number or a bigint for every integer code.
The struct API
Every public name in struct, wherever it is declared. Each links to
the page that documents it.
| Name | Kind | Summary |
|---|---|---|
struct.calcsize | function | Calculates the size of the buffer needed to pack the given values according to the specified format. |
struct.iter_unpack | function | Unpacks a buffer as a repeated sequence of fixed-size records until exhausted, returning a list of… |
struct.pack | function | Packs the given arguments into a bytes object according to the specified format. |
struct.pack_from | function | Same as pack() except that instead of accepting arbitrary values after format, it expects the values to be… |
struct.pack_into | function | Packs directly into an existing bytes object at offset, growing it (zero-padded) if it isn’t long enough… |
struct.unpack | function | Unpacks from bytes or a string into a dictionary based on the given format. |
struct.unpack_from | function | The pack_from() equivalent of unpack(). |
Functions
pack()
struct.pack(format: string, ...values: list) -> bytes
Packs the given arguments into a bytes object according to the specified format.
Parameters
format(string)any...— args
Returns bytes
unpack()
struct.unpack(format: string, data: bytes|string, offset: ?number) -> any
Unpacks from bytes or a string into a dictionary based on the given format.
-
You may have to name the different format codes and separate them by a slash
/to return a string indexed dictionary for easy reference of the destructed parts. If a repeater argument is present, then each of the dictionary keys will have a sequence number behind the given name. -
offsetis the index of the bytes/string to begin unpacking from
Important If you do not name an element, numeric indices starting from 1 are used. Be aware that if you have more than one unnamed element, some data is overwritten because the numbering restarts from 1 for each element.
Parameters
format(string)data(bytes|string)offset(?number) — Default value is0
Returns any
pack_from()
struct.pack_from(format: string, args: list) -> bytes
Same as pack() except that instead of accepting arbitrary values after
format, it expects the values to be in a list.
Parameters
format(string)args(list)
Returns bytes
unpack_from()
struct.unpack_from(format: string, data: bytes|string, offset: ?number) -> any
The pack_from() equivalent of unpack(). This function is essentially
the same as unpack() and was kept for symmetry with pack_from().
Parameters
format(string)data(bytes|string)offset(?number) — Default value is0
Returns any
See also: unpack()
pack_into()
struct.pack_into(format: string, buffer: bytes, offset: number, ...values: list) -> number
Packs directly into an existing bytes object at offset, growing it
(zero-padded) if it isn’t long enough and returns the number of elements
written.
This function avoids an allocate-then-copy round trip when assembling a larger buffer field by field.
Parameters
format(string)buffer(bytes)offset(number)any...— values
Returns number
calcsize()
struct.calcsize(format: string) -> number
Calculates the size of the buffer needed to pack the given values
according to the specified format. It raises an error if the format is
invalid or if the values cannot be packed according to the format or if
it contains a * repeat anywhere, since that has no size independent of
actual data.
Parameters
format(string)
Returns number
Raises Error
iter_unpack()
struct.iter_unpack(format: string, data: bytes|string) -> list
Unpacks a buffer as a repeated sequence of fixed-size records until
exhausted, returning a list of dictionaries. The format string must not
contain any * repeaters, since that would make the record size
variable.
Parameters
format(string)data(bytes|string)
Returns list
2022, Richard Ore and The Zuri Contributors
math
import math
This module contains functions and constants to make trigonometric and
non-trigonometric mathematics a breeze. The module also defines a couple
of commonly used scientific and mathematical constants such as PI.
The math API
Every public name in math, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
math.E | constant | represents Euler’s number, the base of natural logarithms |
math.Infinity | constant | Mathematical infinity |
math.LOG_10 | constant | represents the natural logarithm of 10 |
math.LOG_10_E | constant | represents the base 10 logarithm of e |
math.LOG_2 | constant | represents the natural logarithm of 2 |
math.LOG_2_E | constant | represents the base 2 logarithm of e |
math.NaN | constant | Mathematical NaN |
math.PI | constant | represents the ratio of the circumference of a circle to its diameter |
math.ROOT_2 | constant | represents the square root of 2 |
math.ROOT_3 | constant | represents the square root of 3 |
math.ROOT_HALF | constant | represents the square root of 1/2 |
Constants
PI
math.PI
represents the ratio of the circumference of a circle to its diameter
E
math.E
represents Euler’s number, the base of natural logarithms
LOG_10
math.LOG_10
represents the natural logarithm of 10
LOG_10_E
math.LOG_10_E
represents the base 10 logarithm of e
LOG_2
math.LOG_2
represents the natural logarithm of 2
LOG_2_E
math.LOG_2_E
represents the base 2 logarithm of e
ROOT_2
math.ROOT_2
represents the square root of 2
ROOT_3
math.ROOT_3
represents the square root of 3
ROOT_HALF
math.ROOT_HALF
represents the square root of 1/2
Infinity
math.Infinity
Mathematical infinity
NaN
math.NaN
Mathematical NaN
2021, Richard Ore and Zuri contributors
array
import array
This module provides fixed-width typed array classes: twos- complement
integers (Int8Array/UInt8Array through Int64Array/UInt64Array)
and IEEE-754 floating-point numbers (FloatArray, DoubleArray), each
storing its elements packed in the platform byte order. They complement
the bytes object and allow higher-level binary data manipulation
without giving up direct access to the underlying bytes (to_bytes()).
All ten classes share the same interface (indexing, append()/
insert()/remove_at()/set()/pop(), slice(), fill(),
equals(), iteration via for x in arr, and conversion via
to_list()/to_bytes()): see [[array.Int16Array]] for the fully
documented shape every other class in this module follows.
Int64Array/UInt64Array accept and return bigint values (in
addition to plain integers) for anything past what a regular number
can hold exactly: see is_bigint().
Example
import array
var a = array.Int32Array([1, 2, 3])
a.append(4)
for x in a {
echo x
}
echo a.slice(1, 3).to_list() # [2, 3]
The array API
Every public name in array, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
array.DOUBLE_MAX | constant | Maximum value that “should” exist in a list passed to DoubleArray. |
array.DOUBLE_MIN | constant | Minimum value that “should” exist in a list passed to DoubleArray. |
array.DoubleArray | class | class DoubleArray represents an array of 64-bit IEEE-754 floating-point numbers (doubles) in the platform… |
array.FLOAT_MAX | constant | Maximum value that “should” exist in a list passed to FloatArray. |
array.FLOAT_MIN | constant | Minimum value that “should” exist in a list passed to FloatArray. |
array.FloatArray | class | class FloatArray represents an array of 32-bit IEEE-754 floating-point numbers in the platform byte order. |
array.INT16_MAX | constant | Maximum value that “should” exist in a list passed to Int16Array. |
array.INT16_MIN | constant | Minimum value that “should” exist in a list passed to Int16Array. |
array.INT32_MAX | constant | Maximum value that “should” exist in a list passed to Int32Array. |
array.INT32_MIN | constant | Minimum value that “should” exist in a list passed to Int32Array. |
array.INT64_MAX | constant | Maximum value that “should” exist in a list passed to Int64Array. |
array.INT64_MIN | constant | Minimum value that “should” exist in a list passed to Int64Array. |
array.INT8_MAX | constant | Maximum value that “should” exist in a list passed to Int8Array. |
array.INT8_MIN | constant | Minimum value that “should” exist in a list passed to Int8Array. |
array.Int16Array | class | class Int16Array represents an array of twos-complement 16-bit signed integers in the platform byte order. |
array.Int32Array | class | class Int32Array represents an array of twos-complement 32-bit signed integers in the platform byte order. |
array.Int64Array | class | class Int64Array represents an array of twos-complement 64-bit signed integers in the platform byte order. |
array.Int8Array | class | class Int8Array represents an array of twos-complement 8-bit signed integers. |
array.UINT16_MAX | constant | Maximum value that “should” exist in a list passed to UInt16Array. |
array.UINT32_MAX | constant | Maximum value that “should” exist in a list passed to UInt32Array. |
array.UINT64_MAX | constant | Maximum value that “should” exist in a list passed to UInt64Array. |
array.UINT8_MAX | constant | Maximum value that “should” exist in a list passed to UInt8Array. |
array.UInt16Array | class | class UInt16Array represents an array of twos-complement 16-bit unsigned integers in the platform byte order. |
array.UInt32Array | class | class UInt32Array represents an array of twos-complement 32-bit unsigned integers in the platform byte order. |
array.UInt64Array | class | class UInt64Array represents an array of twos-complement 64-bit unsigned integers in the platform byte order. |
array.UInt8Array | class | class UInt8Array represents an array of twos-complement 8-bit unsigned integers. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
array.double | array.* | DoubleArray: a packed array of double-precision IEEE 754 floats, 8 bytes per element. |
array.float | array.* | FloatArray: a packed array of single-precision IEEE 754 floats, 4 bytes per element. |
array.int16 | array.* | Int16Array: a packed array of twos-complement 16-bit signed integers, 2 bytes per element. |
array.int32 | array.* | Int32Array: a packed array of twos-complement 32-bit signed integers, 4 bytes per element. |
array.int64 | array.* | Int64Array: a packed array of twos-complement 64-bit signed integers, 8 bytes per element. |
array.int8 | array.* | Int8Array: a packed array of twos-complement 8-bit signed integers, 1 byte per element. |
array.uint16 | array.* | UInt16Array: a packed array of unsigned 16-bit integers, 2 bytes per element. |
array.uint32 | array.* | UInt32Array: a packed array of unsigned 32-bit integers, 4 bytes per element. |
array.uint64 | array.* | UInt64Array: a packed array of unsigned 64-bit integers, 8 bytes per element. |
array.uint8 | array.* | UInt8Array: a packed array of unsigned 8-bit integers, 1 byte per element. |
2022, Richard Ore and The Zuri Contributors
array.double
import array
Everything here is re-exported by
array, soimport arrayis enough and the names are called asarray.*. Importingarray.doubleon its own works too and reaches the same definitions.
DoubleArray: a packed array of double-precision IEEE 754 floats, 8
bytes per element.
DOUBLE_MAX and DOUBLE_MIN are the largest and smallest finite values
the type can hold.
Constants
DOUBLE_MAX
array.DOUBLE_MAX: number
Maximum value that “should” exist in a list passed to DoubleArray.
DOUBLE_MIN
array.DOUBLE_MIN: number
Minimum value that “should” exist in a list passed to DoubleArray.
Classes
DoubleArray
class array.DoubleArray < Array
class DoubleArray represents an array of 64-bit IEEE-754 floating-point numbers (doubles) in the platform byte order.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - valueable — has a
@to_value(), so it converts to a plain Zuri value
DoubleArray.reverse()
array.DoubleArray.reverse() -> DoubleArray
Returns a new array containing the elements in the original array in reverse order.
Returns DoubleArray
DoubleArray.clone()
array.DoubleArray.clone() -> DoubleArray
Returns a new DoubleArray containing all items from the current array. The new array is a shallow copy of the original array.
Returns DoubleArray
DoubleArray.slice()
array.DoubleArray.slice(start, end) -> DoubleArray
Returns a new DoubleArray over [start, end) of this array (both
default to the whole array; negative bounds count from the end).
Parameters
start(?number)end(?number)
Returns DoubleArray
2021, Richard Ore and Zuri contributors
array.float
import array
Everything here is re-exported by
array, soimport arrayis enough and the names are called asarray.*. Importingarray.floaton its own works too and reaches the same definitions.
FloatArray: a packed array of single-precision IEEE 754 floats, 4
bytes per element.
FLOAT_MAX and FLOAT_MIN are the largest and smallest finite values
the type can hold.
Constants
FLOAT_MAX
array.FLOAT_MAX: number
Maximum value that “should” exist in a list passed to FloatArray.
FLOAT_MIN
array.FLOAT_MIN: number
Minimum value that “should” exist in a list passed to FloatArray.
Classes
FloatArray
class array.FloatArray < Array
class FloatArray represents an array of 32-bit IEEE-754 floating-point numbers in the platform byte order.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - valueable — has a
@to_value(), so it converts to a plain Zuri value
FloatArray.reverse()
array.FloatArray.reverse() -> FloatArray
Returns a new array containing the elements in the original array in reverse order.
Returns FloatArray
FloatArray.clone()
array.FloatArray.clone() -> FloatArray
Returns a new FloatArray containing all items from the current array. The new array is a shallow copy of the original array.
Returns FloatArray
FloatArray.slice()
array.FloatArray.slice(start, end) -> FloatArray
Returns a new FloatArray over [start, end) of this array (both default
to the whole array; negative bounds count from the end).
Parameters
start(?number)end(?number)
Returns FloatArray
2021, Richard Ore and Zuri contributors
array.int16
import array
Everything here is re-exported by
array, soimport arrayis enough and the names are called asarray.*. Importingarray.int16on its own works too and reaches the same definitions.
Int16Array: a packed array of twos-complement 16-bit signed integers,
2 bytes per element.
INT16_MAX and INT16_MIN bound what an element may be; a value
outside them raises rather than wrapping.
Constants
INT16_MAX
array.INT16_MAX: number = 32767
Maximum value that “should” exist in a list passed to Int16Array.
INT16_MIN
array.INT16_MIN: number
Minimum value that “should” exist in a list passed to Int16Array.
Classes
Int16Array
class array.Int16Array < Array
class Int16Array represents an array of twos-complement 16-bit signed integers in the platform byte order.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - valueable — has a
@to_value(), so it converts to a plain Zuri value
Int16Array.reverse()
array.Int16Array.reverse() -> Int16Array
Returns a new array containing the elements in the original array in reverse order.
Returns Int16Array
Int16Array.clone()
array.Int16Array.clone() -> Int16Array
Returns a new Int16Array containing all items from the current array. The new array is a shallow copy of the original array.
Returns Int16Array
Int16Array.slice()
array.Int16Array.slice(start, end) -> Int16Array
Returns a new Int16Array over [start, end) of this array (both default
to the whole array; negative bounds count from the end).
Parameters
start(?number)end(?number)
Returns Int16Array
2021, Richard Ore and Zuri contributors
array.int32
import array
Everything here is re-exported by
array, soimport arrayis enough and the names are called asarray.*. Importingarray.int32on its own works too and reaches the same definitions.
Int32Array: a packed array of twos-complement 32-bit signed integers,
4 bytes per element.
INT32_MAX and INT32_MIN bound what an element may be; a value
outside them raises rather than wrapping.
Constants
INT32_MAX
array.INT32_MAX: number = 2147483647
Maximum value that “should” exist in a list passed to Int32Array.
INT32_MIN
array.INT32_MIN: number
Minimum value that “should” exist in a list passed to Int32Array.
Classes
Int32Array
class array.Int32Array < Array
class Int32Array represents an array of twos-complement 32-bit signed integers in the platform byte order.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - valueable — has a
@to_value(), so it converts to a plain Zuri value
Int32Array.reverse()
array.Int32Array.reverse() -> Int32Array
Returns a new array containing the elements in the original array in reverse order.
Returns Int32Array
Int32Array.clone()
array.Int32Array.clone() -> Int32Array
Returns a new Int32Array containing all items from the current array. The new array is a shallow copy of the original array.
Returns Int32Array
Int32Array.slice()
array.Int32Array.slice(start, end) -> Int32Array
Returns a new Int32Array over [start, end) of this array (both default
to the whole array; negative bounds count from the end).
Parameters
start(?number)end(?number)
Returns Int32Array
2021, Richard Ore and Zuri contributors
array.int64
import array
Everything here is re-exported by
array, soimport arrayis enough and the names are called asarray.*. Importingarray.int64on its own works too and reaches the same definitions.
Int64Array: a packed array of twos-complement 64-bit signed integers,
8 bytes per element.
INT64_MAX and INT64_MIN bound what an element may be; a value
outside them raises rather than wrapping.
Constants
INT64_MAX
array.INT64_MAX: bigint
Maximum value that “should” exist in a list passed to Int64Array.
Written with the explicit n bigint suffix since the bare literal
9223372036854775807 parses as a lossy number (a known, pre-existing
limitation of plain integer literals at exactly i64::MAX: see
tests/big-integer-literal-overflow.zu), 193 away from the true value.
INT64_MIN
array.INT64_MIN: bigint
Minimum value that “should” exist in a list passed to Int64Array.
Classes
Int64Array
class array.Int64Array < Array
class Int64Array represents an array of twos-complement 64-bit signed integers in the platform byte order.
Values at or beyond ±2^53 need Zuri’s bigint type to be represented
exactly (a regular number is an IEEE-754 double, and loses precision
past that point): append()/set() accept either a plain integer or a
bigint (is_bigint()) for that reason.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - valueable — has a
@to_value(), so it converts to a plain Zuri value
Int64Array.reverse()
array.Int64Array.reverse() -> Int64Array
Returns a new array containing the elements in the original array in reverse order.
Returns Int64Array
Int64Array.clone()
array.Int64Array.clone() -> Int64Array
Returns a new Int64Array containing all items from the current array. The new array is a shallow copy of the original array.
Returns Int64Array
Int64Array.slice()
array.Int64Array.slice(start, end) -> Int64Array
Returns a new Int64Array over [start, end) of this array (both default
to the whole array; negative bounds count from the end).
Parameters
start(?number)end(?number)
Returns Int64Array
2021, Richard Ore and Zuri contributors
array.int8
import array
Everything here is re-exported by
array, soimport arrayis enough and the names are called asarray.*. Importingarray.int8on its own works too and reaches the same definitions.
Int8Array: a packed array of twos-complement 8-bit signed integers, 1
byte per element.
INT8_MAX and INT8_MIN bound what an element may be; a value outside
them raises rather than wrapping.
Constants
INT8_MAX
array.INT8_MAX: number = 127
Maximum value that “should” exist in a list passed to Int8Array.
INT8_MIN
array.INT8_MIN: number
Minimum value that “should” exist in a list passed to Int8Array.
Classes
Int8Array
class array.Int8Array < Array
class Int8Array represents an array of twos-complement 8-bit signed integers.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - valueable — has a
@to_value(), so it converts to a plain Zuri value
Int8Array.reverse()
array.Int8Array.reverse() -> Int8Array
Returns a new array containing the elements in the original array in reverse order.
Returns Int8Array
Int8Array.clone()
array.Int8Array.clone() -> Int8Array
Returns a new Int8Array containing all items from the current array. The new array is a shallow copy of the original array.
Returns Int8Array
Int8Array.slice()
array.Int8Array.slice(start, end) -> Int8Array
Returns a new Int8Array over [start, end) of this array (both default
to the whole array; negative bounds count from the end).
Parameters
start(?number)end(?number)
Returns Int8Array
2021, Richard Ore and Zuri contributors
array.uint16
import array
Everything here is re-exported by
array, soimport arrayis enough and the names are called asarray.*. Importingarray.uint16on its own works too and reaches the same definitions.
UInt16Array: a packed array of unsigned 16-bit integers, 2 bytes per
element.
UINT16_MAX and UINT16_MIN bound what an element may be; a value
outside them raises rather than wrapping.
Constants
UINT16_MAX
array.UINT16_MAX: number = 65535
Maximum value that “should” exist in a list passed to UInt16Array.
Classes
UInt16Array
class array.UInt16Array < Array
class UInt16Array represents an array of twos-complement 16-bit unsigned integers in the platform byte order.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - valueable — has a
@to_value(), so it converts to a plain Zuri value
UInt16Array.reverse()
array.UInt16Array.reverse() -> UInt16Array
Returns a new array containing the elements in the original array in reverse order.
Returns UInt16Array
UInt16Array.clone()
array.UInt16Array.clone() -> UInt16Array
Returns a new UInt16Array containing all items from the current array. The new array is a shallow copy of the original array.
Returns UInt16Array
UInt16Array.slice()
array.UInt16Array.slice(start, end) -> UInt16Array
Returns a new UInt16Array over [start, end) of this array (both
default to the whole array; negative bounds count from the end).
Parameters
start(?number)end(?number)
Returns UInt16Array
2021, Richard Ore and Zuri contributors
array.uint32
import array
Everything here is re-exported by
array, soimport arrayis enough and the names are called asarray.*. Importingarray.uint32on its own works too and reaches the same definitions.
UInt32Array: a packed array of unsigned 32-bit integers, 4 bytes per
element.
UINT32_MAX and UINT32_MIN bound what an element may be; a value
outside them raises rather than wrapping.
Constants
UINT32_MAX
array.UINT32_MAX: number = 4294967295
Maximum value that “should” exist in a list passed to UInt32Array.
Classes
UInt32Array
class array.UInt32Array < Array
class UInt32Array represents an array of twos-complement 32-bit unsigned integers in the platform byte order.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - valueable — has a
@to_value(), so it converts to a plain Zuri value
UInt32Array.reverse()
array.UInt32Array.reverse() -> UInt32Array
Returns a new array containing the elements in the original array in reverse order.
Returns UInt32Array
UInt32Array.clone()
array.UInt32Array.clone() -> UInt32Array
Returns a new UInt32Array containing all items from the current array. The new array is a shallow copy of the original array.
Returns UInt32Array
UInt32Array.slice()
array.UInt32Array.slice(start, end) -> UInt32Array
Returns a new UInt32Array over [start, end) of this array (both
default to the whole array; negative bounds count from the end).
Parameters
start(?number)end(?number)
Returns UInt32Array
2021, Richard Ore and Zuri contributors
array.uint64
import array
Everything here is re-exported by
array, soimport arrayis enough and the names are called asarray.*. Importingarray.uint64on its own works too and reaches the same definitions.
UInt64Array: a packed array of unsigned 64-bit integers, 8 bytes per
element.
UINT64_MAX and UINT64_MIN bound what an element may be; a value
outside them raises rather than wrapping.
Constants
UINT64_MAX
array.UINT64_MAX: bigint
Maximum value that “should” exist in a list passed to UInt64Array.
Classes
UInt64Array
class array.UInt64Array < Array
class UInt64Array represents an array of twos-complement 64-bit unsigned integers in the platform byte order.
Values at or beyond 2^53 need Zuri’s bigint type to be represented
exactly (a regular number is an IEEE-754 double, and loses precision
past that point): append()/set() accept either a plain integer or a
bigint (is_bigint()) for that reason.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - valueable — has a
@to_value(), so it converts to a plain Zuri value
UInt64Array.reverse()
array.UInt64Array.reverse() -> UInt64Array
Returns a new array containing the elements in the original array in reverse order.
Returns UInt64Array
UInt64Array.clone()
array.UInt64Array.clone() -> UInt64Array
Returns a new UInt64Array containing all items from the current array. The new array is a shallow copy of the original array.
Returns UInt64Array
UInt64Array.slice()
array.UInt64Array.slice(start, end) -> UInt64Array
Returns a new UInt64Array over [start, end) of this array (both
default to the whole array; negative bounds count from the end).
Parameters
start(?number)end(?number)
Returns UInt64Array
2021, Richard Ore and Zuri contributors
array.uint8
import array
Everything here is re-exported by
array, soimport arrayis enough and the names are called asarray.*. Importingarray.uint8on its own works too and reaches the same definitions.
UInt8Array: a packed array of unsigned 8-bit integers, 1 byte per
element.
UINT8_MAX and UINT8_MIN bound what an element may be; a value
outside them raises rather than wrapping.
Constants
UINT8_MAX
array.UINT8_MAX: number = 255
Maximum value that “should” exist in a list passed to UInt8Array.
Classes
UInt8Array
class array.UInt8Array < Array
class UInt8Array represents an array of twos-complement 8-bit unsigned integers.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - valueable — has a
@to_value(), so it converts to a plain Zuri value
UInt8Array.reverse()
array.UInt8Array.reverse() -> UInt8Array
Returns a new array containing the elements in the original array in reverse order.
Returns UInt8Array
UInt8Array.clone()
array.UInt8Array.clone() -> UInt8Array
Returns a new UInt8Array containing all items from the current array. The new array is a shallow copy of the original array.
Returns UInt8Array
UInt8Array.slice()
array.UInt8Array.slice(start, end) -> UInt8Array
Returns a new UInt8Array over [start, end) of this array (both default
to the whole array; negative bounds count from the end).
Parameters
start(?number)end(?number)
Returns UInt8Array
2021, Richard Ore and Zuri contributors
set
import set
This module provides functionalities for working with mathematical sets.
Sets are collections of unique values. You can iterate through sets in
the same order in which they were initialized or set by
set.Set.add(). Two Sets containing exactly the same
elements, irrespective of order, are equal, whether compared with ==
or with set.Set.equals().
The example below shows a brief introduction to working with sets.
import set
var my_set = set()
my_set.add(11) # <Set(1) {11}>
my_set.add(56) # <Set(2) {11, 56}>
my_set.add(97) # <Set(3) {11, 56, 97}>
my_set.add('some text') # <Set(4) {11, 56, 97, some text}>
var o = { a: 1, b: 2 }
my_set.add(o)
# o is referencing a different object, but contains the same value
# so it will not be added
my_set.add({ a: 1, b: 2 })
my_set.contains(11) # true
my_set.contains(32) # false, since 32 has not been added to the set
my_set.contains(97) # true
my_set.contains(121 ** 0.5) # true
my_set.contains('Some Text'.lower()) # true
my_set.contains(o) # true
Sets objects can also be created with initial values (or their actual values) by passing a list or a dictionary to the constructor. If a list is passed, all unique elements in the list will be added to the Set object.
For example,
import set
var set_a = set([1, 2, 3, 4, 5])
var set_b = set([4, 5, 6, 7, 8])
echo set_a.intersect(set_b) # <Set(2) {4, 5}>
When a dictionary is passed to the Set constructor, the dictionary keys which are unique themselves will be added to the Set object.
For example:
import set
var set_a = set({
a: 10,
b: 21,
})
echo set_a # <Set(2) {a, b}>
The module is callable. set(...) is the same call as set.set(...),
documented below.
The set API
Every public name in set, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
set.Set | class | The Set class provides some methods that allow you to compose sets like you would with mathematical… |
set.set | function | Default export function for the set.Set class. |
Functions
set()
set.set(items: ?list|dict) -> set.Set
Default export function for the set.Set class.
Parameters
items(?list|dict)
Returns set.Set
Classes
Set
class set.Set
The Set class provides some methods that allow you to compose sets like you would with mathematical operations.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - valueable — has a
@to_value(), so it converts to a plain Zuri value - numeric — converts to a number
Constructor
set.Set(items: ?list|dict)
Creates a new Set object from a list or dictionary or an empty Set object when no argument is passed.
Parameters
items(?list|dict)
Set.union()
set.Set.union(other: instance) -> set.Set
Returns a new set containing elements which are in either or both of this set and the given set.
Parameters
other(set.Set)
Returns set.Set
Set.intersect()
set.Set.intersect(other: instance) -> set.Set
Returns a new set containing elements in both this set and the given set.
Parameters
other(set.Set)
Returns set.Set
Set.difference()
set.Set.difference(other: instance) -> set.Set
Returns a new set containing elements in this set but not in the given set.
Parameters
other(set.Set)
Returns set.Set
Set.symmetric_difference()
set.Set.symmetric_difference(other: instance) -> set.Set
Returns a new set containing elements which are in either this set or the given set, but not in both.
Parameters
other(set.Set)
Returns set.Set
Set.is_disjoint()
set.Set.is_disjoint(other: instance) -> bool
Returns a boolean indicating if this set has no elements in common with the given set.
Parameters
other(set.Set)
Returns bool
Set.is_subset()
set.Set.is_subset(other: instance) -> bool
Returns a boolean indicating if all elements of this set are in the given set.
Parameters
other(set.Set)
Returns bool
Set.is_superset()
set.Set.is_superset(other: instance) -> bool
Returns a boolean indicating if all elements of the given set are in this set.
Parameters
other(set.Set)
Returns bool
Set.is_empty()
set.Set.is_empty() -> bool
Returns a boolean value indicating whether this set is an empty set or not.
Returns bool
Set.contains()
set.Set.contains(value) -> bool
Returns a boolean asserting whether an element is present with the given value in the Set or not.
Parameters
value(any)
Returns bool
Set.length()
set.Set.length() -> number
Returns the number of values in the Set object.
Returns number
Set.clear()
set.Set.clear() -> bool
Removes all elements from the Set object.
Returns bool
Set.remove()
set.Set.remove(value) -> bool
Removes the element associated to the value and returns a boolean
asserting whether an element was successfully removed or not. Once an
element is removed, calling set.contains(value) will return false
afterwards.
Parameters
value(any)
Returns bool
Set.pop()
set.Set.pop() -> any
Removes and returns an arbitrary element from the Set.
Returns any
Raises Error if the Set is empty.
Set.add()
set.Set.add(value) -> bool
Inserts a new element with the specified value in to the Set object, if there isn’t an element with the same value already in the Set.
Parameters
value(any)
Returns bool
Set.clone()
set.Set.clone() -> set.Set
Returns a new Set which is an exact replica of the current Set.
Returns set.Set
Set.equals()
set.Set.equals(other: instance) -> bool
Returns a boolean indicating whether this Set and other contain
exactly the same elements, irrespective of order. == makes the same
comparison.
Parameters
other(set.Set)
Returns bool
Set.update()
set.Set.update(other: instance) -> set.Set
Adds every element of other to this Set in place, and returns this Set
(so calls can chain). Unlike union(), this mutates self instead of
returning a new Set.
Parameters
other(set.Set)
Returns set.Set
Set.intersection_update()
set.Set.intersection_update(other: instance) -> set.Set
Keeps only the elements this Set has in common with other, in place,
and returns this Set. Unlike intersect(), this mutates self instead
of returning a new Set.
Parameters
other(set.Set)
Returns set.Set
Set.difference_update()
set.Set.difference_update(other: instance) -> set.Set
Removes every element other also contains from this Set, in place, and
returns this Set. Unlike difference(), this mutates self instead of
returning a new Set.
Parameters
other(set.Set)
Returns set.Set
Set.symmetric_difference_update()
set.Set.symmetric_difference_update(other: instance) -> set.Set
Keeps only the elements in exactly one of this Set or other, in place,
and returns this Set. Unlike symmetric_difference(), this mutates
self instead of returning a new Set.
Parameters
other(set.Set)
Returns set.Set
Set.each()
set.Set.each(callback: function)
Calls function callback once for each value present in the Set, in
insertion order.
Parameters
callback(function)
Set.to_string()
set.Set.to_string() -> string
Returns a string that represents the current Set.
Returns string
Set.to_list()
set.Set.to_list() -> list
Returns the current Set as a list of elements.
Returns list
Richard Ore, 2025
date
import date
This modules provides Zuri’s implementation of date and time manipulation methods. This module implements civil dates as well as julian dates.
Definitions
-
The calendar date (
class Date) is a particular day of a calendar year, identified by its ordinal number within a calendar month within that year. -
The Julian date number (
jd) is in elapsed days and time since noon (Greenwich Mean Time) on January 1, 4713 BCE (in the Julian calendar).
Date instances support ordering (<, <=, >, >=) and equals()
by comparing the underlying instant in time: see [[date.Date.equals]]
for how that interacts with gmt_offset. Arithmetic is done through
named methods rather than +/- (add_seconds, add_days,
add_months, …; see [[date.Date.add_seconds]] and
[[date.Date.add_months]]) since a plain number added to a date is
otherwise ambiguous about its unit.
The module is callable. date(...) is the same call as
date.date(...), documented below.
The date API
Every public name in date, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
date.Date | class | Date and Time manipulation class |
date.MAX_DAY | constant | Maximum day supported. |
date.MAX_HOUR | constant | Maximum hour supported. |
date.MAX_MINUTE | constant | Maximum minute supported. |
date.MAX_MONTH | constant | Maximum year supported. |
date.MAX_SECONDS | constant | Maximum seconds supported. |
date.MAX_YEAR | constant | Maximum year supported. |
date.MIN_DAY | constant | Minimum day supported. |
date.MIN_MONTH | constant | Minimum month supported. |
date.MIN_YEAR | constant | Minimum year supported. |
date.date | function | Returns a new Date instance representing the given system date or the current date if no argument is… |
date.from_jd | function | Returns a date instance representing the julian date. |
date.from_time | function | Returns a date object from a unix timestamp. |
date.from_timezone | function | Returns a Date from wall-clock fields understood as a local time IN the real IANA timezone name, rather… |
date.gmtime | function | Returns a dictionary representing the current time without timezone adjustment. |
date.is_valid_timezone | function | Returns true when name is a real IANA timezone identifier (e.g. 'Africa/Lagos', 'America/New_York',… |
date.list_timezones | function | Returns every IANA timezone identifier this build’s timezone database recognizes, e.g. 'Africa/Lagos',… |
date.localtime | function | Returns a dictionary representing the current time after adjusting for the current timezone |
date.mktime | function | Convert the broken-out time into a time value with the same encoding as that of the values returned by the… |
date.parse | function | Parses a date string into a Date instance, automatically detecting the format. |
date.parse_format | function | Parses a date string using an explicit format pattern and returns a Date instance. |
Constants
MIN_YEAR
date.MIN_YEAR: number = 1
Minimum year supported.
MAX_YEAR
date.MAX_YEAR: number = 9999
Maximum year supported.
MIN_DAY
date.MIN_DAY: number = 1
Minimum day supported.
MAX_DAY
date.MAX_DAY: number = 31
Maximum day supported.
MIN_MONTH
date.MIN_MONTH: number = 1
Minimum month supported.
MAX_MONTH
date.MAX_MONTH: number = 12
Maximum year supported.
MAX_HOUR
date.MAX_HOUR: number = 23
Maximum hour supported.
MAX_MINUTE
date.MAX_MINUTE: number = 59
Maximum minute supported.
MAX_SECONDS
date.MAX_SECONDS: number = 59
Maximum seconds supported.
Functions
gmtime()
date.gmtime()
Returns a dictionary representing the current time without timezone adjustment.
Example,
%> echo date.gmtime()
{year: 2022, month: 3, day: 5, week_day: 6, year_day: 63, hour: 17, minute: 30,
seconds: 55, microseconds: 620290, is_dst: false, zone: UTC, gmt_offset: 0}
Returns — dictionary
Note:
year_dayhere is the platform’s owntm_yday, counting Jan 1st as day 0.Date.year_daycounts it as day 1; the two differ by one on purpose, since this dictionary is the raw reading andDateis the module’s own representation.
localtime()
date.localtime()
Returns a dictionary representing the current time after adjusting for the current timezone
Example:
%> echo date.localtime()
{year: 2022, month: 3, day: 5, week_day: 6, year_day: 63, hour: 18, minute: 18,
seconds: 35, microseconds: 598166, is_dst: false, zone: WAT, gmt_offset: 3600}
Returns — dictionary
mktime()
date.mktime(year, month, day, hour, minute, seconds, is_dst) -> number
Convert the broken-out time into a time value with the same encoding as that of the values returned by the time() function (that is, seconds from the Epoch, UTC) according to the timezone settings.
Example:
%> import date
%> echo date.mktime(2021, 2, 12, 13, 43, 11, false)
1613133791
Parameters
year(number)month(number)day(number)hour(number)minute(number)seconds(number)is_dst(bool)
Returns number
is_valid_timezone()
date.is_valid_timezone(name) -> bool
Returns true when name is a real IANA timezone identifier (e.g.
'Africa/Lagos', 'America/New_York', 'UTC'), backed by the timezone
database embedded in this build. This is a real, complete IANA database:
unlike parse()’s own timezone handling, which only recognizes
UTC/GMT/Z/numeric offsets and treats every other name as an
unvalidated label: so this is the function to reach for whenever a
timezone name actually needs checking.
%> date.is_valid_timezone('Africa/Lagos')
true
%> date.is_valid_timezone('Neverland/Nowhere')
false
Parameters
name(string)
Returns bool
Raises TypeError When name is not a string.
list_timezones()
date.list_timezones() -> list[string]
Returns every IANA timezone identifier this build’s timezone database
recognizes, e.g. 'Africa/Lagos', 'America/New_York', 'UTC'. There
are several hundred of these; this is mainly useful for populating a
picker or validating against the full set at once rather than one name
at a time.
Returns list[string]
from_time()
date.from_time(time) -> Date
Returns a date object from a unix timestamp.
Example,
%> date.from_time(time()).to_string()
'<Date year: 2022, month: 3, day: 5, hour: 18, minute: 34, seconds: 1>'
Parameters
time(number)
Returns Date
Note: Time must be in seconds.
from_timezone()
date.from_timezone(name, year, month, day, hour, minute, seconds, is_dst) -> Date
Returns a Date from wall-clock fields understood as a local time IN
the real IANA timezone name, rather than in UTC (the plain Date(...)
constructor) or the process’s own system zone (mktime()). The offset,
zone abbreviation, and DST state are all worked out from name’s real
rules at that wall-clock moment.
A wall-clock reading can be ambiguous once a year: the hour a clock
falls back repeats, e.g. 1:30am happens twice on the day
America/New_York leaves daylight saving: is_dst picks which
occurrence is meant, the same way it does for mktime(). A reading a
clock skips entirely (the hour a clock springs forward) has no valid
answer at all and raises, rather than silently returning a nearby
instant.
var d = date.from_timezone('America/New_York', 2024, 7, 1, 0, 0, 0)
echo d.to_time() # the UTC instant of 2024-07-01 00:00:00 EDT
echo d.zone # EDT
echo d.gmt_offset # -14400
Parameters
name(string) — An IANA timezone identifier.year(number)month(number)day(number)hour(number)minute(number)seconds(number)is_dst(?bool) — Disambiguates a wall-clock reading that occurs twice; ignored otherwise. Defaults to the earlier occurrence when omitted.
Returns Date
Raises TypeError When name is not a string, or a date/time field
is not a number.
Raises Error When name is not a recognized timezone, the fields
are out of range, or the wall-clock reading falls in a DST gap.
from_jd()
date.from_jd(jdate) -> number
Returns a date instance representing the julian date.
Example,
%> date.from_jd(22063).to_string()
'<Date year: 2022, month: 3, day: 5, hour: 18, minute: 35, seconds: 0>'
Parameters
jdate(number)
Returns number
parse()
date.parse(date_string) -> Date
Parses a date string into a Date instance, automatically detecting the
format.
parse recognises a broad set of standard date and datetime formats
without requiring the caller to specify the format string:
Recognised formats
| Category | Examples |
|---|---|
| ISO 8601 extended | 2024-06-15, 2024-06-15T14:30:00Z |
| ISO 8601 extended | 2024-06-15 14:30:00+01:00 |
| ISO 8601 compact | 20240615, 20240615T143000Z |
| RFC 2822 | Thu, 21 Dec 2000 16:01:07 +0200 |
| HTTP date | Sat, 05 Mar 2022 06:23:32 GMT |
| Full month name first | March 05, 2022 6:24 PM |
| Full month name first | January 1 2000 |
| Day-month-year | 21 Dec 2000 16:01:07 +0200 |
| US slash-separated | 06/15/2024, 6/15/2024 2:30 PM |
| EU dot-separated | 15.06.2024, 15.06.2024 14:30:00 |
| Hyphen numeric | 15-06-2024 |
| ISO order slash | 2024/06/15 |
| Time only | 14:30:00, 2:30 PM, 14:30:00.123456 UTC |
Timezone handling
When a timezone offset is present in the string it is stored in
gmt_offset (seconds) and zone (name string). When absent, zone
defaults to "UTC" and gmt_offset to 0.
Ambiguity policy
For slash-separated numeric dates where both the first and second fields
are ≤ 12 (e.g. 06/05/2024), the US convention is assumed: month first
(MM/DD/YYYY). When the input is known to be DD/MM/YYYY, use
parse_format with '%d/%m/%Y' instead.
Example
import date
var d = date.parse('2024-06-15T14:30:00Z')
echo d.year # 2024
echo d.month # 6
echo d.hour # 14
echo d.zone # UTC
var d2 = date.parse('March 05, 2022 6:24 PM')
echo d2.year # 2022
echo d2.hour # 18 (PM applied)
var d3 = date.parse('Thu, 21 Dec 2000 16:01:07 +0200')
echo d3.year # 2000
echo d3.gmt_offset # 7200
Parameters
date_string(string) — The date string to parse.
Returns Date
Raises TypeError When date_string is not a string.
Raises ValueError When the string cannot be parsed as a date.
parse_format()
date.parse_format(date_string, format) -> Date
Parses a date string using an explicit format pattern and returns a
Date instance.
The format pattern uses the same character codes as the Date.format()
method, making it straightforward to round-trip a date through
format() and back through parse_format().
Format character reference
The following characters are interpreted as directives in the format
string. All other characters are treated as literals and must appear
verbatim in the input. Prefix a character with a backslash (\) to
force literal treatment even for directive characters.
| Character | Consumes | Example input |
|---|---|---|
Y | 4-digit year | 2024 y |
| → 1970–1999) | 24 m | Month with leading zero (01–12) |
| Month without leading zero (1–12) | 6 M | Abbreviated month name |
| (case-insensitive) | Jun F | Full month name (case-insensitive) |
June d | Day with leading zero (01–31) | 05 j |
| leading zero (1–31) | 5 D | Abbreviated weekday name (consumed and |
| ignored) | Thu l | Full weekday name (consumed and ignored) |
Thursday H | 24-hour hour with leading zero (00–23) | 14 G |
| 24-hour hour without leading zero (0–23) | 14 h | 12-hour hour with |
| leading zero (01–12) | 02 g | 12-hour hour without leading zero |
| (1–12) | 2 i | Minutes with leading zero (00–59) |
| Seconds with leading zero (00–59) | 00 u | Microseconds (up to 6 |
| digits) | 123456 v | Milliseconds (up to 3 digits) |
| Uppercase AM/PM | PM a | Lowercase am/pm |
| name/identifier | UTC O | Timezone offset without colon |
P | Timezone offset with colon | +02:00 Z |
| seconds (signed integer) | 7200 c | ISO 8601 full datetime |
(delegated to parse) | 2024-06-15T14:30:00+02:00 r | RFC 2822 full |
datetime (delegated to parse) | Thu, 21 Dec 2000 16:01:07 +0200 S | |
| Ordinal suffix (consumed and ignored) | th | |
t,L,N,w,z,W,I | Output-only fields (skipped) | : |
Example
import date
# Basic date
var d = date.parse_format('15/06/2024', 'd/m/Y')
echo d.year # 2024
echo d.month # 6
echo d.day # 15
# DateTime with AM/PM
var d2 = date.parse_format('March 05, 2022 6:24 PM', 'F d, Y g:i A')
echo d2.hour # 18
# Round-trip through format()
var original = date.Date(2024, 6, 15, 14, 30, 0)
var fmt = 'D, d M Y H:i:s O'
var reparsed = date.parse_format(original.format(fmt), fmt)
echo reparsed.day # 15
echo reparsed.month # 6
# European DD/MM/YYYY (ambiguous with auto-detect: use parse_format)
var eu = date.parse_format('05/06/2024', 'd/m/Y')
echo eu.month # 6
echo eu.day # 5
# Timezone-aware
var tz = date.parse_format('2024-06-15T14:30:00+05:30', 'Y-m-d\\TH:i:sP')
echo tz.gmt_offset # 19800 (5.5 hours in seconds)
Parameters
date_string(string) — The input string to parse.format(string) — The format pattern describingdate_string.
Returns Date
Raises TypeError When either argument is not a string.
Raises ValueError When the string does not match the expected
format.
date()
date.date(year, month, day, hour, minute, seconds) -> Date
Returns a new Date instance representing the given system date or the
current date if no argument is specified.
Parameters
year(?number)month(?number)day(?number)hour(?number)minute(?number)seconds(?number)is_dst(?bool)
Returns Date
Classes
Date
class date.Date
Date and Time manipulation class
A date here refers to a calendar datetime consisting of year, month, day, hour, minute and seconds.
The Date class manages both Date and DateTime and this module does not
make any distinction between the two as Date is a subset of DateTime.
Example,
%> import date
%> var d = date(2021)
%> d.to_string()
'<Date year: 2021, month: 1, day: 1, hour: 0, minute: 0, seconds: 0>'
%> d = date()
%> d.to_string()
'<Date year: 2022, month: 3, day: 5, hour: 19, minute: 25, seconds: 58>'
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode() - numeric — converts to a number
Constructor
date.Date(year, month, day, hour, minute, seconds, microseconds, gmt_offset, is_dst)
Parameters
year(?number)month(?number)day(?number)hour(?number)minute(?number)seconds(?number)microseconds(?number)gmt_offset(?number)is_dst(?bool)
Note: All arguments are optional
Note: When no argument is given, the date will be set to the current system date.
Note: when the date fields are given, the timezone is always in UTC irrespective of the system timezone or if a gmt_offset is given or not.
Note:
year_dayon the resulting instance counts Jan 1st as day 1, which is whatto_ordinal()andformat('z')expect. The raw dictionaries fromlocaltime()andgmtime()are the one place that differs: those carry the platform’s owntm_yday, which counts Jan 1st as day 0.
Date.is_leap()
date.Date.is_leap() -> bool
Returns true if the year is a leap year or false otherwise.
Example,
%> date(2018).is_leap()
false
%> date(2020).is_leap()
true
Returns bool
Date.to_ordinal()
date.Date.to_ordinal() -> number
Returns this date’s ordinal day number in the proleptic Gregorian
calendar, where day 1 is 0001-01-01. Two dates can be compared or
subtracted via their ordinals to get an exact day count between them,
regardless of leap years.
Example,
%> date(1, 1, 1).to_ordinal()
1
%> date(2021, 5, 11).to_ordinal()
737921
Returns number
Date.days_before_month()
date.Date.days_before_month(month) -> number
Returns the number of days between the first day of month (in this
date’s own year) and this date. Positive when month is later in the
year than this date, negative when earlier, zero when month is this
date’s own month and day is 1.
Example,
%> date(2021, 5, 11).days_before_month(7)
51
Returns number
Date.days_before_year()
date.Date.days_before_year(year) -> number
Returns the number of days between January 1st of year and this date.
Positive when year is after this date’s own year, negative when before
(or the same year but earlier in it).
Example,
%> date(2021, 5, 11).days_before_year(2024)
965
Parameters
year(int)
Returns number
Date.days_in_month()
date.Date.days_in_month() -> number
Returns the number of days in month for the specified year.
Example,
%> date(2021, 6).days_in_month()
30
Returns number
Date.weekday()
date.Date.weekday() -> number
Returns the numbered day of the week.
Example,
%> date(2021, 5, 11).weekday()
2
Returns number
Date.week_number()
date.Date.week_number() -> number
Returns the number of the current week in the year.
Example,
%> date(2021, 5, 11).week_number()
19
Returns number
Date.format()
date.Date.format(format) -> string
Formats the current date based on the specified string
Zuri’s Date formatting table
| Character | Description | Example |
|---|---|---|
| A | uppercase Ante meridian and Post meridian | AM or PM a |
| Ante meridian and Post meridian | am or pm d | day of the month with |
| leading zero | 01 to 31 D | textual representation of a day, three |
| letters | Mon - Sun j | day of the month without leading zero |
| l | full textual representation of the day of the week | Monday - Sunday |
| N | ISO-8601 numeric representation of the day of the week | 1 - 7 S |
| English ordinal suffix for the day of the month | st, nd, rd or th w | |
| numeric representation of the day of the week | 0 - 6 z | the day of the |
| year (starting from 0) | 0 - 365 W | ISO-8601 week number of year, weeks |
| starting on Monday | E.g. 33 (the 33rd week of the year) F | full |
| textual representation of a month | January - December m | numeric |
| representation of a month, with leading zeros | 01 - 12 n | numeric |
| representation of a month, without leading zeros | 1 - 12 M | short |
| textual representation of a month, three letters | Jan - Dec t | number |
| of days in the given month | 28 - 31 L | whether it’s a leap year |
| true, 0 otherwise y | two digit representation of a year | e.g. 09 or 99 |
| Y | full numeric representation of a year using 4 digits | e.g. 2009 or |
| 1999 h | 12 hour format of an hour with leading zeros | 01 - 12 H |
| hour format of an hour with leading zeros | 01 - 24 g | 12 hour format |
| of an hour without leading zeros | 1 - 12 G | 24 hour format of an hour |
| without leading zeros | 1 - 24 i | minutes with leading zero |
| seconds with leading zero | 00 - 59 u | microseconds |
| milliseconds | e.g. 987 e | timezone identifier |
| whether or not the date is in daylight saving time | 1 for true, 0 | |
| otherwise O | difference to GMT without colon between hours and minutes | |
| e.g. +0100 P | difference to GMT with colon between hours and minutes | |
| e.g. +01:00 Z | timezone offset in seconds | -43200 - 50400 c |
| 8601 date | e.g. 2020-03-04T15:19:21+00:00 r | RFC 2822 formatted date |
| e.g. Thu, 21 Dec 2000 16:01:07 +0200 |
Example,
%> date().format('F d, Y g:i A')
'March 05, 2022 6:24 PM'
You can prevent a format character in the format string from being expanded by escaping it with a preceding backslash. If the character with a backslash is already a special sequence, you may need to also escape the backslash.
For example:
%> date().format('l jS \o\\f F Y h:i:s A')
'Wednesday 17th of May 2021 01:39:08 PM'
Parameters
format(string)
Returns string
Date.http()
date.Date.http() -> string
Returns the HTTP date representation of the current date.
For example,
%> date().http()
'Sat, 05 Mar 2022 06:23:32 GMT'
Returns string
Date.jd()
date.Date.jd() -> number
Converts the current date to a julian day and time.
Example,
%> date(2021, 5, 11).jd()
2459345
Returns number
Date.unix_time()
date.Date.unix_time() -> number
Returns unix mktime equivalent of the current date.
Returns number
Deprecated. Use
to_time()instead as it offers more precision.
Date.to_time()
date.Date.to_time() -> number
Returns the Epoch timestamp in seconds for the given date.
Returns number
Date.clone()
date.Date.clone() -> Date
Returns an independent copy of this date.
Returns Date
Date.equals()
date.Date.equals(other) -> bool
Returns true when other is a Date representing the exact same
instant in time as this one. Two dates with different
gmt_offset/zone labels but the same underlying UTC instant are
considered equal: the same convention Python’s timezone-aware datetime
equality uses.
Parameters
other(any)
Returns bool
Date.diff()
date.Date.diff(other) -> number
Returns the signed difference, in seconds, between this date and other
(self.to_time() - other.to_time()). Positive when this date is later
than other, negative when earlier.
Parameters
other(Date)
Returns number
Date.add_seconds()
date.Date.add_seconds(n) -> Date
Returns a new Date, n seconds after this one (or before, if n is
negative). The result keeps this date’s own
gmt_offset/zone/is_dst. self is not modified.
Parameters
n(number)
Returns Date
Date.add_minutes()
date.Date.add_minutes(n) -> Date
Returns a new Date, n minutes after this one. See add_seconds().
Parameters
n(number)
Returns Date
Date.add_hours()
date.Date.add_hours(n) -> Date
Returns a new Date, n hours after this one. See add_seconds().
Parameters
n(number)
Returns Date
Date.add_days()
date.Date.add_days(n) -> Date
Returns a new Date, n days after this one. See add_seconds().
Parameters
n(number)
Returns Date
Date.add_months()
date.Date.add_months(n) -> Date
Returns a new Date, n months after this one (or before, if n is
negative), keeping the same time-of-day and
gmt_offset/zone/is_dst. self is not modified.
When the resulting month doesn’t have this date’s own day number (e.g. Jan 31 plus one month), the result is clamped to the last valid day of that month (Jan 31 + 1 month → Feb 28 or 29, never an overflow into March): the convention to know about, since date libraries genuinely differ on this.
Parameters
n(number)
Returns Date
Date.add_years()
date.Date.add_years(n) -> Date
Returns a new Date, n years after this one. See add_months() for
the day-clamping rule this also follows (relevant for Feb 29 plus a
non-leap number of years).
Parameters
n(number)
Returns Date
Date.to_offset()
date.Date.to_offset(gmt_offset, zone) -> Date
Returns a new Date representing the exact same instant as this one,
re-expressed under a different gmt_offset. This is the way to convert
a date from one timezone’s wall-clock reading to another’s: unlike
add_seconds() and friends, which shift the underlying instant, this
keeps the instant fixed and only changes how it’s displayed.
Parameters
gmt_offset(number) — Seconds east of UTC.zone(?string) — Optional label for the new offset (e.g.'EST'); when omitted, this date’s ownzoneis kept as-is.
Returns Date
Date.to_timezone()
date.Date.to_timezone(name) -> Date
Returns a new Date representing the exact same instant as this one,
re-expressed under the real IANA timezone name (e.g.
'America/New_York', 'Africa/Lagos'). Unlike to_offset(), which
takes the numeric offset to shift to and leaves it to the caller to
already know it, this looks up name’s actual offset: correctly
accounting for daylight saving: at this date’s own instant, so
gmt_offset, zone, and is_dst all come out right without the caller
needing to work any of that out first.
var d = date.parse('2024-07-01T00:00:00Z')
var ny = d.to_timezone('America/New_York')
echo ny.hour # 20 (the previous day, EDT is UTC-4)
echo ny.zone # EDT
echo ny.is_dst # true
Parameters
name(string) — An IANA timezone identifier.
Returns Date
Raises TypeError When name is not a string.
Raises Error When name is not a recognized timezone (see
is_valid_timezone()).
Date.to_string()
date.Date.to_string() -> string
Returns a string representation of the date
Returns string
Date.to_dict()
date.Date.to_dict() -> dict
Returns the date object as a dictionary.
Returns dict
Date.to_number()
date.Date.to_number()
2021, Richard Ore and Zuri contributors
os
import os
The os module is Zuri’s interface to the underlying operating system:
environment variables, the filesystem, other processes, and the machine
itself. Per-file operations: reading, writing, deleting, or copying one
particular file, or inspecting its size/permissions/timestamps: live on
the file class instead; see file.stats(), file.delete(), and
file.copy(). Everything that isn’t about one specific already-open
file lives here.
Environment variables
import os
var port = os.get_env('PORT', '8080')
os.set_env('APP_ENV', 'production')
for name in os.environ() {
echo '${name} = ${os.environ()[name]}'
}
Paths
Paths are resolved and compared purely as strings, without touching the filesystem, so they work the same whether or not anything actually exists at the path yet.
var config = os.join_paths(os.home_dir(), '.config', 'zuri')
var absolute = os.abs_path('../lib/main.zu')
var rel = os.relative_path('/srv/app', '/srv/app/logs/out.log')
# rel == 'logs/out.log'
The filesystem
os.create_dir('build')
for entry in os.read_dir('.', false) {
if os.is_dir(entry) continue
echo entry
}
var git = os.which('git') # full path, or nil if not on PATH
var sources = os.glob('*.zu')
Running other programs
exec() is the simple case: run a command, block until it finishes, get
back its exit code and output.
var result = os.exec('git', 'rev-parse', 'HEAD')
if result.exit_code == 0 {
echo 'HEAD is at ${result.output.trim()}'
}
spawn() is for everything exec() doesn’t cover: a long-running
command, one that needs input written to it, or one whose output needs
to be read as it arrives rather than all at once at the end.
var proc = os.spawn('grep', ['error'], { stdin: 'pipe' })
proc.write_stdin('startup ok\nerror: disk full\n')
proc.close_stdin()
echo proc.read_stdout().to_string() # 'error: disk full\n'
proc.wait()
on_signal() traps SIGINT/SIGTERM/etc. so a long-running script can
shut down cleanly instead of being torn down mid-work. Falling off the
end of the callback lets the signal do what it would have done anyway,
so this cleans up and then exits:
os.on_signal('INT', @() {
echo 'shutting down...'
flush_caches()
})
Return a truthy value instead to swallow the signal and keep running.
Windows has no signals. A callback registered there runs for console
events like Ctrl+C, and never for anything kill() sends.
The machine
echo 'running on ${os.num_cpus()} cores'
echo '${os.free_memory() / 1_000_000} MB free of ${os.total_memory() / 1_000_000} MB'
Scratch space
create_temp_file()/create_temp_dir() reserve a uniquely named path
under temp_dir() atomically, so two processes racing to create scratch
space can never collide on the same name:
var path = os.create_temp_file('upload-', '.tmp')
file(path, 'w').write(incoming_data)
# ... use it, then clean up when done
file(path).delete()
The os API
Every public name in os, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
os.DT_BLK | constant | Block device file type |
os.DT_CHR | constant | Character device file type |
os.DT_DIR | constant | Directory file type |
os.DT_FIFO | constant | Named pipe file type |
os.DT_LNK | constant | Symbolic link file type |
os.DT_REG | constant | Regular file type |
os.DT_SOCK | constant | Local-domain socket file type |
os.DT_UNKNOWN | constant | Unknown file type |
os.DT_WHT | constant | Whiteout file type (only meaningful on UNIX and some unofficial Linux versions). |
os.Process | class | A running (or finished) child process created by spawn(). |
os.abs_path | function | Returns the absolute form of path: relative paths are resolved against the current working directory, and… |
os.args | constant | The command line, as the running script sees it. |
os.at_exit | function | Registers callback to run when the program ends. |
os.base_name | function | The base_name() function returns the last component from the pathname pointed to by path, deleting any… |
os.change_dir | function | Navigates the working directory into the specified path. |
os.chmod | function | Changes the permission set on a directory to the given mode. |
os.chown | function | Changes the owning user and group of path to uid and gid. |
os.create_dir | function | Creates the given directory with the specified permission and optionally add new files into it if any is… |
os.create_temp_dir | function | Atomically creates a new, empty, uniquely named directory inside temp_dir() and returns its path. |
os.create_temp_file | function | Atomically creates a new, empty, uniquely named file inside temp_dir() and returns its path. |
os.cwd | function | The current working directory. |
os.dir_exists | function | Returns true if path exists and is a directory, false otherwise (including when path exists but is a… |
os.dir_name | function | Returns the parent directory of the pathname pointed to by path. |
os.environ | function | Returns every environment variable currently visible to this process as a dictionary of name/value pairs. |
os.exe_path | constant | The full path to the running Zuri executable. |
os.exec | function | Executes the given shell (or command prompt for Windows) commands and returns a dictionary containing the… |
os.exit | function | Exit the current process and quits the Zuri runtime. |
os.expand_user | function | Expands a leading ~ in path into the current user’s home directory, the same way a shell would before… |
os.expand_vars | function | Expands $NAME/$NAME`` references in text (and, on Windows, %NAME% references as well) using the… |
os.free_memory | function | An estimate of how much physical memory is available for new allocations right now, in bytes: free memory… |
os.get_env | function | Returns the given environment variable if it exists or default_value (nil if not given) otherwise. |
os.glob | function | Returns every entry under base_path (the current working directory if not given) whose path matches the… |
os.home_dir | function | The current machine user’s home directory. |
os.hostname | function | The current machine’s hostname. |
os.info | function | Returns information about the current operation system and machine as a dictionary. |
os.is_dir | function | Returns true if the path is a directory or false otherwise. |
os.is_symlink | function | Returns true if path exists and is a symbolic link, false otherwise (including when path doesn’t… |
os.join_paths | function | Concatenates the given paths together into a format that is valid on the current operating system. |
os.kill | function | Sends signal to the process identified by pid. |
os.num_cpus | function | The number of logical CPUs available to this process. |
os.on_signal | function | Registers callback to run when this process receives the named signal, for handling things like Ctrl+C… |
os.path_contains | function | Returns true if the resolved form of candidate is base itself or lies somewhere underneath it, and… |
os.path_separator | constant | The standard path separator for the current operating system. |
os.pid | function | The current process’s id. |
os.platform | constant | The name of the current platform in string or unknown if the platform name could not be determined. |
os.ppid | function | The current process’s parent’s id. |
os.read_dir | function | Scans the given directory and returns a list of the names it contains, sorted by name, led by . and ... |
os.readlink | function | Returns the target path points to, if path is a symbolic link. |
os.real_path | function | Returns the original path to a relative path. |
os.relative_path | function | Returns the relative path from base to target: the shortest ./..-based path such that, starting… |
os.remove_dir | function | Deletes a non-empty directory. |
os.rename | function | Renames the file or directory specified by old_name to the name given by new_name. |
os.set_env | function | Sets the named environment variable to the given value. |
os.set_exit_code | function | Records the status the process should end with, without ending it. |
os.sleep | function | Causes the current thread to sleep for the specified number of seconds. |
os.spawn | function | Spawns cmd as a new subprocess and returns a Process handle to it immediately, without waiting for it to… |
os.target | constant | The platform the running runtime was built for, as a target triple: x86_64-unknown-linux-gnu,… |
os.temp_dir | function | The platform’s directory for temporary files (e.g. /tmp on Unix, whatever %TEMP% points to on Windows). |
os.total_memory | function | The total physical memory installed on this machine, in bytes. |
os.umask | function | Gets or sets the process’s file-creation mask: the set of permission bits stripped from every file/directory… |
os.unset_env | function | Removes the named environment variable, if it’s set. |
os.uptime | function | How long the machine has been running since it last booted, in seconds. |
os.version | constant | The current Zuri version. |
os.vm_version | constant | The current Zuri VM version. |
os.which | function | Searches every directory in the PATH environment variable, in order, for an executable file named name,… |
Submodules
| Module | Reached as | Summary |
|---|---|---|
os.env | os.* | Reading, writing, and enumerating the current process’s environment variables. |
os.fs | os.* | Directory and filesystem-entry operations: creating, listing, and removing directories, permissions and… |
os.path | os.* | Path string manipulation: joining, resolving, and comparing paths. |
os.process | os.* | Process identity, subprocess execution, and signal handling. |
os.system | os.* | Facts about the current process, the Zuri runtime, and the machine it’s running on. |
os.tempfile | os.* | Locating the platform’s temporary-file directory and creating uniquely named scratch files/directories inside… |
2021, Richard Ore and Zuri contributors
os.env
import os
Everything here is re-exported by
os, soimport osis enough and the names are called asos.*. Importingos.envon its own works too and reaches the same definitions.
Reading, writing, and enumerating the current process’s environment variables.
Functions
get_env()
os.get_env(name: string, default_value) -> ?string
Returns the given environment variable if it exists or default_value
(nil if not given) otherwise.
Example,
%> import os
%> os.get_env('ENV1')
'20'
Parameters
name(string)default_value(?any)
Returns ?string
set_env()
os.set_env(name: string, value: string, overwrite: ?bool) -> bool
Sets the named environment variable to the given value.
Example,
%> os.set_env('ENV1', 'New value')
true
%> os.get_env('ENV1')
'New value'
If you are in the REPL and have tried the last example in get_env(),
you may notice that the value of ENV1 doesn’t change. This is because
unless you specify, set_env() will not overwrite existing environment
variables. For that, you will need to specify true as the third
parameter to set_env().
For example,
%> os.set_env('ENV1', 'New value again', true)
true
%> os.get_env('ENV1')
'New value again'
Parameters
name(string)value(string)overwrite(?bool) — Default value isfalse.
Returns bool
Note: Environment variables set will not persist after application exists.
unset_env()
os.unset_env(name: string)
Removes the named environment variable, if it’s set.
Example,
%> os.set_env('ENV1', '20')
true
%> os.unset_env('ENV1')
true
%> os.get_env('ENV1')
nil
%> os.unset_env('ENV1')
false
Parameters
name(string)
Returns — bool: true if the variable existed and was removed,
false if it wasn’t set to begin with.
Note: Just like
set_env(), this change does not persist past the lifetime of the current process.
environ()
os.environ() -> dict
Returns every environment variable currently visible to this process as a dictionary of name/value pairs.
Example,
%> os.environ()
{HOME: /home/username, SHELL: /bin/bash, ...}
Returns dict
Note: the returned dictionary is a snapshot taken at the time of the call; later calls to
set_env()/unset_env()don’t retroactively change a dictionary you’re already holding.
expand_vars()
os.expand_vars(text: string) -> string
Expands $NAME/${NAME} references in text (and, on Windows,
%NAME% references as well) using the current process’s environment,
the same way a shell would before running a command.
A reference to a variable that isn’t set expands to an empty string, matching typical shell behavior, rather than being left untouched or raising.
text is meant to come from somewhere that isn’t itself a Zuri string
literal: a config file, a template on disk, a value passed in on the
command line: since a ${NAME} written directly in Zuri source is
interpolated by Zuri’s own string syntax before expand_vars() ever
sees it.
Example,
%> os.set_env('NAME', 'zuri')
true
%> os.expand_vars('hello, $NAME!')
'hello, zuri!'
%> os.expand_vars(file('greeting.template').read())
'hello, zuri!'
Parameters
text(string)
Returns string
2021, Richard Ore and Zuri contributors
os.fs
import os
Everything here is re-exported by
os, soimport osis enough and the names are called asos.*. Importingos.fson its own works too and reaches the same definitions.
Directory and filesystem-entry operations: creating, listing, and
removing directories, permissions and ownership, symbolic links,
searching PATH for an executable, and glob-style pattern matching.
Per-file operations (reading, writing, deleting, or copying a single
file, or getting its size/mtime/etc.) live on the file class instead:
see file.stats(), file.delete(), file.copy(), and friends.
Constants
DT_UNKNOWN
os.DT_UNKNOWN: number
Unknown file type
DT_BLK
os.DT_BLK: number
Block device file type
DT_CHR
os.DT_CHR: number
Character device file type
DT_DIR
os.DT_DIR: number
Directory file type
DT_FIFO
os.DT_FIFO: number
Named pipe file type
DT_LNK
os.DT_LNK: number
Symbolic link file type
DT_REG
os.DT_REG: number
Regular file type
DT_SOCK
os.DT_SOCK: number
Local-domain socket file type
DT_WHT
os.DT_WHT: number
Whiteout file type (only meaningful on UNIX and some unofficial Linux versions).
Note: value is
-1on systems where it is not supported.
Functions
create_dir()
os.create_dir(path: string, permission: ?int, recursive: ?bool) -> boolean
Creates the given directory with the specified permission and optionally add new files into it if any is given.
Parameters
path(string)permission(?int) — Default value is0c777recursive(?bool) — Default value istrue.
Returns boolean
Note: if the directory already exists, it returns
falseotherwise, it returnstrue.
Note: permission should be given as octal number.
read_dir()
os.read_dir(path: string, recursive: ?bool) -> list[string]
Scans the given directory and returns a list of the names it contains,
sorted by name, led by . and ...
Example,
%> os.read_dir('./tests')
[., .., buggy.zu, myprogram.zu, single_thread.zu, test.zu]
When the recursive argument is set to true, sub-directories are
descended into as well, and their contents come back as paths relative
to path rather than as bare names. A directory’s contents follow
immediately after the directory itself, and entries are sorted by name
within every level:
%> os.read_dir('./tests', true)
[., .., buggy.zu, data, data/input.csv, test.zu]
A symbolic link to a directory is listed but not descended into, whether
or not it points somewhere inside path.
Parameters
path(string)recursive(?bool) — Default isfalse.
Returns list[string]
Note:
.indicates current directory and can be used as argument to os.read_dir as well.
Note:
..indicates parent directory and can be used as argument to os.read_dir as well.
chmod()
os.chmod(path: string, mode: int) -> boolean
Changes the permission set on a directory to the given mode. It is advisable to set the mode with an octal number (e.g. 0c777) as this is consistent with operating system values.
Parameters
path(string)mode(int)
Returns boolean
is_dir()
os.is_dir(path: string) -> bool
Returns true if the path is a directory or false otherwise.
Parameters
path(string)
Returns bool
remove_dir()
os.remove_dir(path: string, recursive: ?bool) -> bool
Deletes a non-empty directory. If recursive is true, non-empty
directories will have their contents deleted first.
Parameters
path(string)recursive(?bool) — Default value isfalse.
Returns bool
dir_exists()
os.dir_exists(path: string) -> bool
Returns true if path exists and is a directory, false otherwise
(including when path exists but is a regular file or some other
non-directory entry).
Parameters
path(string)
Returns bool
rename()
os.rename(old_name: string, new_name: string) -> bool
Renames the file or directory specified by old_name to the name given
by new_name.
If old_name and new_name are existing hard links referring to the
same file, then it does nothing, and returns a success status.
If old_name specifies a directory, new_name must either not exist,
or it must specify an empty directory.
If old_name refers to a symbolic link, the link is renamed; if
new_name refers to a symbolic link, the link will be overwritten.
Parameters
old_name(string)new_name(string)
Returns bool
Raises Error
chown()
os.chown(path: string, uid: int, gid: int) -> bool
Changes the owning user and group of path to uid and gid.
Parameters
path(string)uid(int)gid(int)
Returns bool
Raises Error
Note: this is a Unix-only operation; ownership isn’t a concept Windows has a direct equivalent for, so this raises there.
umask()
os.umask(mask: ?int) -> int
Gets or sets the process’s file-creation mask: the set of permission bits stripped from every file/directory this process creates from now on.
Called with no argument (or nil), returns the current mask without
changing it. Called with a mask, sets it and returns whatever the
previous mask was.
Parameters
mask(?int)
Returns int
Raises Error
Note: this is a Unix-only operation; Windows has no umask concept, and this raises there.
is_symlink()
os.is_symlink(path: string) -> bool
Returns true if path exists and is a symbolic link, false
otherwise (including when path doesn’t exist at all).
Parameters
path(string)
Returns bool
readlink()
os.readlink(path: string) -> string
Returns the target path points to, if path is a symbolic link.
Unlike os.real_path(), this reads exactly one link level and doesn’t
recursively resolve further: if the target is itself a symlink, its own
target is what gets returned, not the final destination.
Parameters
path(string)
Returns string
Raises Error if path doesn’t exist or isn’t a symbolic link.
which()
os.which(name: string) -> ?string
Searches every directory in the PATH environment variable, in order,
for an executable file named name, the same way a shell decides what
running a bare command name actually runs. Returns the full path to the
first match, or nil if none of them have a matching executable.
Example,
%> os.which('git')
'/usr/bin/git'
%> os.which('does-not-exist')
nil
Parameters
name(string)
Returns ?string
glob()
os.glob(pattern: string, base_path: ?string) -> list[string]
Returns every entry under base_path (the current working directory if
not given) whose path matches the glob pattern.
pattern supports * (anything except a path separator), ? (exactly
one character, except a path separator), and a doubled star (anything,
including path separators, for matching across nested directories).
Example,
%> os.glob('*.zu')
['helper.zu', 'main.zu']
%> os.glob('lib?.zu')
['libc.zu', 'libz.zu']
Matches are returned in the order read_dir() walks the tree, which
means sorted by name within each directory, with a directory’s own
matches following it.
Parameters
pattern(string)base_path(?string) — Default is the current working directory.
Returns list[string]
2021, Richard Ore and Zuri contributors
os.path
import os
Everything here is re-exported by
os, soimport osis enough and the names are called asos.*. Importingos.pathon its own works too and reaches the same definitions.
Path string manipulation: joining, resolving, and comparing paths.
Nothing in this file touches the filesystem except where explicitly
documented (real_path(), and abs_path()’s optional strict mode):
everything else here is pure string handling, so it works the same
whether or not the path in question actually exists.
Functions
join_paths()
os.join_paths(...paths: list) -> string
Concatenates the given paths together into a format that is valid on the current operating system.
Example,
%> os.join_paths('/home/user', 'path/to/myfile.ext')
'/home/user/path/to/myfile.ext'
Parameters
string...— paths
Returns string
real_path()
os.real_path(path: string) -> string
Returns the original path to a relative path.
Parameters
path(string)
Returns string
Note: if the path is a file, see
abs_path().
abs_path()
os.abs_path(path: string, strict: ?bool) -> string
Returns the absolute form of path: relative paths are resolved against
the current working directory, and “.” / “..” segments are collapsed:
purely as string manipulation, with no filesystem access at all by
default. Unlike real_path(), this works even when path (or any part
of it) doesn’t exist on disk, and it never resolves symbolic links; use
real_path() when you need the true canonical path of something that
already exists.
On Windows, a path that starts at a root without naming a drive, such as
\\srv\\app or /srv/app, is on the working directory’s drive, and a
UNC path such as \\\\server\\share\\app keeps \\\\server\\share as
its root, which .. never climbs out of.
Parameters
path(string)strict(?bool) — Defaultfalse. Whentrue, the resolved path is checked against the filesystem and anErroris raised if nothing exists there.
Returns string
dir_name()
os.dir_name(path: string) -> string
Returns the parent directory of the pathname pointed to by path. Any
trailing / characters are not counted as part of the directory name.
If path is an empty string, or contains no / characters, dir_name()
returns the string “.”, signifying the current directory.
On Windows, a drive (C:) or UNC share (\\\\server\\share) at the
start of path stays whole: the parent of C:\\ is C:\\ itself, the
parent of C:\\work is C:\\, and the parent of C:work is C:. The
root’s parent is the root, just as / is its own parent elsewhere, so a
loop that walks up until the parent stops changing ends there.
Parameters
path(string)
Returns string
base_name()
os.base_name(path: string) -> string
The base_name() function returns the last component from the pathname
pointed to by path, deleting any trailing / characters. If path
consists entirely of / characters, the string ‘/’ is returned. If path
is an empty string, the string ‘.’ is returned.
Parameters
path(string)
Returns string
relative_path()
os.relative_path(base: string, target: string) -> string
Returns the relative path from base to target: the shortest
./..-based path such that, starting inside base, following it
lands on target. Both arguments are first resolved with abs_path(),
so neither has to already be absolute or exist on disk.
Example,
%> os.relative_path('/home/user/project', '/home/user/project/src/main.zu')
'src/main.zu'
%> os.relative_path('/home/user/project/src', '/home/user/other/lib.zu')
'../../other/lib.zu'
%> os.relative_path('/home/user/project', '/home/user/project')
'.'
Parameters
base(string)target(string)
Returns string
expand_user()
os.expand_user(path: string) -> string
Expands a leading ~ in path into the current user’s home directory,
the same way a shell would before running a command. A bare ~ and a
~/...-prefixed path are both expanded; a path that doesn’t start with
~ is returned unchanged.
Parameters
path(string)
Returns string
Note: expanding another user’s home directory (
~other_user/...) isn’t supported; a path in that form is returned unchanged, the same fallback Python’sos.path.expanduseruses when it can’t resolve one either.
path_contains()
os.path_contains(base: string, candidate: string) -> bool
Returns true if the resolved form of candidate is base itself or
lies somewhere underneath it, and false otherwise. This includes when
candidate escapes base via .. segments. Both paths are resolved
with abs_path() first, so relative paths and mixed separators are
handled the same way abs_path() itself handles them.
Example,
%> os.path_contains('/home/user/project', '/home/user/project/src/main.zu')
true
%> os.path_contains('/home/user/project', '/home/user/project/../../etc/passwd')
false
Parameters
base(string)candidate(string)
Returns bool
home_dir()
os.home_dir() -> ?string
The current machine user’s home directory. If the home directory cannot
be detected, it returns nil.
Returns ?string
cwd()
os.cwd() -> string
The current working directory.
Returns string
change_dir()
os.change_dir(path: string) -> bool
Navigates the working directory into the specified path.
Parameters
path(string)
Returns bool
2021, Richard Ore and Zuri contributors
os.process
import os
Everything here is re-exported by
os, soimport osis enough and the names are called asos.*. Importingos.processon its own works too and reaches the same definitions.
Process identity, subprocess execution, and signal handling.
Functions
exec()
os.exec(...args: list) -> string
Executes the given shell (or command prompt for Windows) commands and returns a dictionary containing the exit code and the output as string.
exec() accepts multiple string arguments and concatenates them
together with a space in between before executing.
The returned dictionary contains the following:
exit_code: number - The exit code of the program. Any number other than zero (0) should be treated as a failure. -output: string - The output of the program.
exec() is the simple, blocking one-shot convenience: run a command,
wait for it, get back its exit code and combined output. spawn() is
for everything exec() doesn’t cover: streaming a child’s stdout/stderr
as it’s produced, writing to its stdin, overriding its working directory
or environment, or checking on it without blocking.
Example,
%> os.exec('ls', '-l')
{exit_code: 1, output: 'total 48
-rw-r--r--@ 1 username staff 705 Aug 27 2021 buggy.zu
-rw-r--r-- 1 username staff 197 Mar 5 05:13 myprogram.zu'}
Parameters
cmd(...string)
Returns string
Note: this blocks until the command finishes and buffers its entire output in memory; for a long-running command, one you need to feed input to, or one whose output you want to process as it arrives, use
spawn()instead.
exit()
os.exit(code: number)
Exit the current process and quits the Zuri runtime.
Parameters
code(number)
Returns
set_exit_code()
os.set_exit_code(code: number)
Records the status the process should end with, without ending it.
Unlike exit(), the program carries on: the rest of the script, every
at_exit() handler, and an uncaught error’s report all still get their
turn, and the status is applied once they are done.
import os
os.at_exit(@{
echo 'one check did not pass'
os.set_exit_code(1)
})
echo 'working'
working
one check did not pass
Parameters
code(number)
Note: an uncaught error still ends the program with
1, whatever was recorded here.
Note: the last call wins.
at_exit()
os.at_exit(callback: function)
Registers callback to run when the program ends.
Handlers run last registered first, whether the program reached the end
of its script, called exit(), or stopped on an uncaught error.
Registering several is fine, and a handler may register another one
which will also run.
import os
var handle = file('report.txt', 'w')
os.at_exit(@{
handle.close()
echo 'report written'
})
handle.write('done')
echo 'working'
working
report written
Parameters
callback(function)
Note: a handler that raises is reported on standard error and the remaining handlers still run, so one failed cleanup cannot cancel the others.
Note:
exit()called from inside a handler exits immediately with that code; the handlers that have not run yet do not run.
Note: a handler does not run when the process is killed by a signal nothing handled, since nothing gets to run at that point.
pid()
os.pid() -> int
The current process’s id.
Returns int
ppid()
os.ppid() -> int
The current process’s parent’s id.
Returns int
Note: Windows has no call that asks for this directly. It is answered there by searching a snapshot of the running processes for this one and reading the parent recorded against it, which yields
0rather than raising if the snapshot cannot be taken or this process is not in it.
kill()
os.kill(pid: int, signal: ?int) -> bool
Sends signal to the process identified by pid.
Signal 0 sends nothing. It runs the same existence and permission
checks every other signal does and then stops, which makes it the way to
ask whether a process is still running and still yours to signal:
var alive = true
catch {
os.kill(os.pid(), 0)
} as e {
alive = false
}
echo alive # true
Parameters
pid(int)signal(?int) — Default15(SIGTERM), matching thekill(1)command’s own default.
Returns bool
Raises Error if pid doesn’t name a process this one has
permission to signal, or if signal is one this platform cannot carry
out.
Note: Windows has no way to signal a process by id, so it accepts only the numbers it can carry out exactly:
0to test for the process, and9or15to end it. Every other number raises there rather than ending the process anyway, because ending a process outright is a different outcome from asking it to stop, not a near-enough one. A process ended this way never runs its ownon_signal()callback and gets no chance to clean up. Its exit status is128 + signal, the same number a Unix caller would have read for it.
on_signal()
os.on_signal(name: string, callback: function) -> bool
Registers callback to run when this process receives the named signal,
for handling things like Ctrl+C gracefully instead of being torn down
immediately.
callback takes no arguments. It runs on the main thread, between
bytecode instructions: never from a background thread: so it’s safe for
it to do anything ordinary Zuri code can do, including raising, which
propagates the same way an uncaught error from any other code would.
What the callback returns decides what happens next
Returning a truthy value means the callback has taken responsibility for the signal, and the program carries on:
os.on_signal('INT', def {
echo 'ignoring Ctrl+C, still working'
return true
})
Returning anything falsy means it hasn’t, and the signal then gets the
default action it would have had if nothing were registered: for
'INT', 'TERM' and 'HUP', terminating the process with the
conventional 128 + signal number status. A callback that just cleans
up and falls off the end returns nil, which is falsy, so it shuts down
without needing to call exit():
os.on_signal('INT', def {
echo 'shutting down...'
flush_caches()
})
That default is deliberate. A handler that only logged and always kept running would leave Ctrl+C unable to stop the program at all, which is a much worse thing to do by accident than to shut down.
Truthiness here is the language’s own, so return 0 and return ''
decline exactly as return false does. Call os.exit() when you want a
specific exit status instead.
Supported signal names are 'INT' (Ctrl+C), 'TERM', 'HUP' (Unix
only), and 'BREAK' (Windows only); names are matched
case-insensitively, and carry no SIG prefix. Registering a new
callback for a name that already has one replaces it: there’s no
stacking of multiple handlers for the same signal.
On Windows
Windows has no signals. A callback registered here becomes a console
control handler, so it runs for console events and nothing else: 'INT'
for Ctrl+C, 'BREAK' for Ctrl+Break, and 'TERM' for the console
window being closed. Nothing another process does with kill() ever
reaches it, including this process calling kill() on its own id.
'TERM' is weaker there than it looks. The console is closing either
way, so Windows allows the callback a few seconds and then ends the
process regardless of what it returned: a truthy return cannot keep the
program running, the way it can for a real SIGTERM. Treat 'TERM' on
Windows as a short window to flush what matters, not as a signal you can
decline.
Parameters
name(string)callback(function)
Returns bool
Raises Error if name isn’t a recognized signal name, or isn’t
supported on the current platform.
Note: only takes effect for the main thread’s own execution; code running inside an
isolateworker isn’t affected by a handler registered here.
Note: a signal delivered during a blocking native call (
sleep(), a blockingProcess.wait(), blocking file/network I/O, …) isn’t seen until that call returns and bytecode execution resumes, the same limitation every cooperative signal-handling system has.
spawn()
os.spawn(cmd: string, args: ?list, options: ?dict) -> Process
Spawns cmd as a new subprocess and returns a Process handle to it
immediately, without waiting for it to finish.
cmd names a program to run, not a command line for a shell to
interpret. Nothing here expands a variable, splits on whitespace,
follows a pipe or knows what a shell builtin is, so a name only a shell
would recognise raises rather than running: echo is a builtin on
Windows with no executable behind it, and cat is not present there at
all. Use exec() when a shell is what you want.
options may include:
cwd:?string: the child’s working directory. Defaults to this process’s own current working directory.env:?dict: extra environment variables for the child, merged into a copy of this process’s own environment by default (setenv_replace: truefor the child to receive only what’s inenv, nothing inherited).env_replace:?bool: seeenvabove. Defaultfalse.stdin,stdout,stderr:?string, one of'pipe'(readable/writable through the returnedProcess),'inherit'(shares this process’s own stream), or'null'(discarded).stdindefaults to'inherit';stdoutandstderrdefault to'pipe'. Do not confuse'null'withnil.'null'alludes to/dev/nullon unix devices which has the same behavior.
Example,
%> var p = os.spawn('grep', ['zuri'], { stdin: 'pipe' })
%> p.write_stdin('hello zuri\nhello world\n')
true
%> p.close_stdin()
%> p.read_stdout().to_string()
hello zuri
%> p.wait()
0
Parameters
cmd(string)args(?list) — The command’s own arguments (not shell- interpreted the wayexec()’s combined string is: each element is passed to the child as one literal argument).options(?dict)
Returns Process
Raises Error if cmd couldn’t be spawned at all (not found, no
permission, …).
Classes
Process
class os.Process
A running (or finished) child process created by spawn().
Note: constructed only by
spawn()itself: there’s no meaningful way to build one by hand since there’s no “spawn it later” step to defer.
Constructor
os.Process(_ptr)
Process.write_stdin()
os.Process.write_stdin(data) -> bool
Writes data to the child’s stdin.
Parameters
data(string|bytes)
Returns bool
Raises Error if this process’s stdin wasn’t opened with stdin: 'pipe',
or has already been closed via close_stdin().
Process.close_stdin()
os.Process.close_stdin()
Closes the write half of this process’s stdin, so the child sees EOF on
it. Needed for any child that reads its own stdin until EOF: the pipe
only actually closes once this is called (or the Process itself is
garbage collected).
Returns
Process.read_stdout()
os.Process.read_stdout(length: ?int) -> bytes
Reads from the child’s stdout. While the child runs, this blocks until
at least one byte has been produced or its stdout has hit EOF, and
returns what has arrived so far. Once wait() or try_wait() has seen
the child exit, it returns everything the child wrote that has not been
read yet, waiting for its stdout to hit EOF first.
Parameters
length(?int) — The maximum number of bytes to return; all available output if not given.
Returns bytes
Raises Error if this process’s stdout wasn’t opened with stdout: 'pipe'.
Process.read_stderr()
os.Process.read_stderr(length: ?int) -> bytes
Same as read_stdout(), for the child’s stderr instead.
Parameters
length(?int)
Returns bytes
Raises Error if this process’s stderr wasn’t opened with stderr: 'pipe'.
Process.wait()
os.Process.wait(timeout_ms: ?int)
Blocks until the child exits (or timeout_ms elapses, whichever comes
first) and returns its exit code.
Parameters
timeout_ms(?int) — Wait indefinitely if not given.
Returns — ?int: The exit code, or nil if timeout_ms elapsed
before the child exited.
Process.try_wait()
os.Process.try_wait()
Checks whether the child has exited yet, without blocking.
Returns — ?int: The exit code if it has already exited, nil if
it’s still running.
Process.kill()
os.Process.kill(signal: ?int) -> bool
Sends signal to this child process.
Parameters
signal(?int) — Default15(SIGTERM).
Returns bool
Raises Error if signal is one this platform cannot carry out.
Note: Windows accepts only the numbers it can carry out exactly, the same three the free
kill()function takes there:0,9and15. Anything else raises. Signal0is a no-op on a child, since holding this handle is already proof the process exists.
Process.pid()
os.Process.pid() -> int
This child process’s id.
Returns int
2021, Richard Ore and Zuri contributors
os.system
import os
Everything here is re-exported by
os, soimport osis enough and the names are called asos.*. Importingos.systemon its own works too and reaches the same definitions.
Facts about the current process, the Zuri runtime, and the machine it’s running on.
Constants
version
os.version: string
The current Zuri version.
target
os.target: string
The platform the running runtime was built for, as a target triple:
x86_64-unknown-linux-gnu, aarch64-apple-darwin,
x86_64-pc-windows-msvc and so on. Release archives are named by it,
which is what makes it the way to find a runtime for this machine.
%> os.target
'x86_64-unknown-linux-gnu'
vm_version
os.vm_version: string
The current Zuri VM version.
platform
os.platform: string
The name of the current platform in string or unknown if the platform
name could not be determined.
Example,
%> import os
%> os.platform
'osx'
args
os.args: list
The command line, as the running script sees it.
The first entry is the path to the Zuri executable and the second is the
script being run. Everything after those two was meant for the script
itself, so a program reads its own arguments from args[2,] and gets
the same list whether it was launched with zuri run or as a command.
%> import os
%> os.args
['/usr/local/bin/zuri', '/home/ada/greet.zu', '--name', 'Ada']
The args module reads this for you, and reading it directly is only
worth it for the simplest of programs.
path_separator
os.path_separator: string
The standard path separator for the current operating system.
exe_path
os.exe_path: string
The full path to the running Zuri executable.
Functions
sleep()
os.sleep(duration: number)
Causes the current thread to sleep for the specified number of seconds.
Parameters
duration(number)
info()
os.info() -> dict
Returns information about the current operation system and machine as a dictionary. The returned dictionary will contain:
sysname: The name of the operating system -nodenameThe name of the current machine -version: The operating system version -release: The release level/version -machine: The hardware/processor type.
Example,
%> os.info()
{sysname: Darwin, nodename: MacBook-Pro.local, version: Darwin Kernel Version
21.1.0: Wed Oct 13 17:33:24 PDT 2021; root:xnu-8019.41.5~1/RELEASE_ARM64_T8101,
release: 21.1.0, machine: arm64}
Returns dict
num_cpus()
os.num_cpus() -> int
The number of logical CPUs available to this process. Useful for sizing
a worker pool: isolate.cpu_count() is the exact same underlying value,
exposed here too so code that only needs this one fact doesn’t have to
import the whole isolate module for it.
Returns int
hostname()
os.hostname() -> string
The current machine’s hostname.
Returns string
Raises Error if the hostname couldn’t be determined.
total_memory()
os.total_memory() -> number
The total physical memory installed on this machine, in bytes.
Returns number
Raises Error if this isn’t supported on the current platform.
free_memory()
os.free_memory() -> number
An estimate of how much physical memory is available for new allocations
right now, in bytes: free memory plus easily reclaimable caches/buffers,
the same figure a tool like free reports as “available” rather than
the smaller, less useful “free” figure alone.
Returns number
Raises Error if this isn’t supported on the current platform.
uptime()
os.uptime() -> number
How long the machine has been running since it last booted, in seconds.
Returns number
Raises Error if this isn’t supported on the current platform.
2021, Richard Ore and Zuri contributors
os.tempfile
import os
Everything here is re-exported by
os, soimport osis enough and the names are called asos.*. Importingos.tempfileon its own works too and reaches the same definitions.
Locating the platform’s temporary-file directory and creating uniquely
named scratch files/directories inside it without a check-then-create
race: create_temp_file()/create_temp_dir() reserve the path
atomically (the same guarantee the Unix mkstemp() family gives), so
two concurrent callers can never be handed the same path.
Functions
temp_dir()
os.temp_dir() -> string
The platform’s directory for temporary files (e.g. /tmp on Unix,
whatever %TEMP% points to on Windows).
Returns string
create_temp_file()
os.create_temp_file(prefix: ?string, suffix: ?string) -> string
Atomically creates a new, empty, uniquely named file inside temp_dir()
and returns its path. The file is created (and left open-then-closed)
purely to reserve the name without a race between checking whether it
exists and creating it: this function only hands back the path; open it
yourself with file() to actually read or write it.
Parameters
prefix(?string) — Default''.suffix(?string) — Default''.
Returns string
Raises Error if a unique name couldn’t be found, or the file
couldn’t be created.
Note: the caller is responsible for deleting the file (via
file(path).delete()) once it’s no longer needed; nothing cleans temp files up automatically.
create_temp_dir()
os.create_temp_dir(prefix: ?string) -> string
Atomically creates a new, empty, uniquely named directory inside
temp_dir() and returns its path.
Parameters
prefix(?string) — Default''.
Returns string
Raises Error if a unique name couldn’t be found, or the directory
couldn’t be created.
Note: the caller is responsible for removing the directory (via
os.remove_dir(path, true)) once it’s no longer needed; nothing cleans temp directories up automatically.
2021, Richard Ore and Zuri contributors
env
import env
Configuration from a file, in the environment, in the type you wanted.
A program’s configuration belongs in its environment, and a developer’s
machine has nowhere convenient to put it. env closes that gap: it
reads a .env file into the process environment at startup, and it
reads values back out already converted to the number, boolean or list
the program is going to use.
import env
env.load()
var port = env.int('PORT', 8080)
var debug = env.bool('DEBUG', false)
var secret = env.require('SESSION_SECRET')
The file
A .env file sits beside the program and is not committed. One name per
line:
# The address to bind on.
HOST=127.0.0.1
PORT=8080
DATABASE_URL="postgres://app:secret@${HOST}/app"
SESSION_SECRET='r4w $tring, taken exactly as written'
PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqh...
-----END PRIVATE KEY-----"
Unquoted values are trimmed and run to a comment or the end of the line.
Double quotes resolve backslash escapes and allow $ references. Single
quotes and backticks take every character exactly as written. Values in
any of the three quotes may span lines. parse() states the grammar in
full.
What already exists wins
A name that is already set in the process environment is left alone, and the file fills in the rest. This is the whole point of the default: the file carries what a developer needs to run the program at all, and production sets the real values through the shell, the orchestrator or the CI runner without the file having to know.
Result says exactly what happened, and override() reverses the rule
where a file genuinely should win:
var result = env.loader().path('.env').override().load()
echo result.applied # the names this load set
echo result.skipped # the names that were already set
Throughout the module, a variable set to the empty string counts as
unset. FLAG= in a file, an empty shell variable and a name nobody ever
set all mean the same thing, which is the one thing anybody means by
them.
References between values
Values may refer to other values and to the environment around them, with the shell’s syntax and three of its modifiers:
HOST=localhost
PORT=${PORT:-8080}
PUBLIC_URL=http://${HOST}:${PORT}
CACHE_DIR=${XDG_CACHE_HOME:-${HOME}/.cache}/app
SESSION_SECRET=${SESSION_SECRET:?the deploy must supply a session secret}
A reference resolves to the value the name will hold once the load has
finished, so PORT=${PORT:-8080} reads “whatever the environment
already says, and 8080 when it says nothing” no matter which line of the
file defines it. \$ is a literal dollar sign, and a value in single
quotes never expands at all.
Layering files
Sources are read in the order they are added, and the last one to define a name defines it:
env.loader()
.path('.env') # what the team shares, committed as .env.example
.path('.env.local') # what this machine does differently
.load()
source() adds text instead of a file, so configuration that arrived
from a secret store or a test fixture goes through the same parsing,
expansion and precedence.
Reading values back
get() and require() answer with text. int(), float(), bool()
and list() answer with the type and raise a ValueError naming the
variable when the text is not one:
var port = env.int('PORT', 8080)
var timeout = env.float('REQUEST_TIMEOUT', 2.5)
var debug = env.bool('DEBUG', false)
var origins = env.list('CORS_ORIGINS', ',', [])
They read the process environment, not the file, so they answer the same
whether a value came from .env or from the shell. That is what makes
the same program run unchanged in both places.
Do not commit it
A .env file holds the values that differ between one deployment and
the next, which is to say it holds the secrets. Commit a .env.example
with every name and no real value, add .env to .gitignore, and let
required() fail loudly on a machine where the file was never made.
The env API
Every public name in env, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
env.EnvError | class | Base class for every error this module raises. |
env.Loader | class | Builds a load: the sources it reads and the four decisions it makes about them. |
env.MissingFile | class | A file the loader was told to require is not there. |
env.MissingVariable | class | A variable a program said it needed is not configured. |
env.ParseError | class | A source could not be parsed. |
env.Result | class | What a load did. |
env.bool | function | Returns the value of name as a boolean. |
env.expand.expand | function | Replaces every $ reference in value with what resolve answers, and returns the result. |
env.float | function | Returns the value of name as a number. |
env.get | function | Returns the value of name, or default_value when it is unset or empty. |
env.has | function | Returns true when name is set to a non-empty value. |
env.int | function | Returns the value of name as an integer. |
env.is_name | function | Returns true when name is a legal environment variable name. |
env.list | function | Returns the value of name split into a list. |
env.load | function | Reads an environment file and writes what it holds into the process environment. |
env.loader | function | Returns a new Loader, for a load that needs more than the default. |
env.parse | function | Reads the text of an environment file and returns its names and values. |
env.require | function | Returns the value of name, and raises when it is unset or empty. |
env.stringify | function | Turns a dictionary of names and values into the text of an environment file. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
env.errors | env.errors.* | Every error the env module raises, under one root. |
env.expand | import env.expand | Resolving the $ references inside a value. |
env.loading | env.loading.* | The loader: which sources to read, in what order, and what to do with the names that come out of them. |
env.parser | env.parser.* | The scanner that turns the text of an environment file into names and values, and the writer that turns them… |
env.values | env.values.* | Reading configuration back out of the process environment, in the type the program actually wants. |
Functions
loader()
env.loader() -> Loader
Returns a new Loader, for a load that needs more than the default.
import env
var result = env.loader()
.path('.env')
.path('.env.local')
.required()
.load()
Returns Loader
load()
env.load(path: ?string) -> Result
Reads an environment file and writes what it holds into the process environment.
The one line most programs need, and the first line most of them run.
path defaults to .env in the current working directory, a leading
~ is expanded, and a file that is not there leaves the environment as
it stands.
Names already set to a non-empty value are left alone. Loader is where
that, and everything else about a load, can be changed.
import env
env.load()
env.load('config/production.env')
Parameters
path(?string) — Defaults to'.env'.
Returns Result
Raises ParseError when the file is not a valid environment file.
Raises MissingVariable when a ${NAME:?reason} reference finds
nothing.
2026, Richard Ore and Zuri contributors
env.errors
import env.errors
envlifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledenv.errors.*needsimport env.errors.
Every error the env module raises, under one root.
EnvError is that root. The three subclasses separate the cases worth
handling differently: a file that will not parse, a file that was
demanded and is not there, and a variable a program cannot run without.
Each one carries the detail a message alone cannot. ParseError knows
the line and column it stopped at, MissingFile knows the path it
looked for, and MissingVariable knows the name it wanted.
Classes
EnvError
class env.EnvError < Error
Base class for every error this module raises.
Catch this to catch anything env can do.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
env.EnvError(message)
EnvError.to_string()
env.EnvError.to_string()
ParseError
class env.ParseError < EnvError
A source could not be parsed.
line and column are both 1-based and point at the character the
parser gave up on, which is where the fix goes.
catch {
env.parse('12FACTOR=yes')
} as error {
echo '${error.message} (line ${error.line}, column ${error.column})'
}
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
line | number | The 1-based line the parser stopped at. |
column | number | The 1-based column within that line. |
Constructor
env.ParseError(message: string, line: number, column: number)
Parameters
message(string)line(number)column(number)
ParseError.to_string()
env.ParseError.to_string()
MissingFile
class env.MissingFile < EnvError
A file the loader was told to require is not there.
Only Loader.required() produces this. A loader left at its default
treats an absent file as an empty one and records the path in
Result.missing instead.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
path | string | The path that was looked for, after ~ was expanded. |
Constructor
env.MissingFile(path: string)
Parameters
path(string)
MissingFile.to_string()
env.MissingFile.to_string()
MissingVariable
class env.MissingVariable < EnvError
A variable a program said it needed is not configured.
Raised by require() and by the ${NAME:?reason} form during
expansion.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
name | string | The name of the variable that was wanted. |
Constructor
env.MissingVariable(name: string, message: ?string)
Parameters
name(string)message(?string) — A reason to use instead of the default text.
MissingVariable.to_string()
env.MissingVariable.to_string()
2026, Richard Ore and Zuri contributors
env.expand
import env.expand
envdoes not re-export this module, so it is reached only by importing it directly.
Resolving the $ references inside a value.
The syntax is the shell’s, because that is what a .env file looks like
to everyone who has ever written one, and the modifiers are the four
from POSIX parameter expansion that earn their place in a configuration
file: a fallback, an override, and a demand.
Nothing here reads the environment itself. Every lookup goes through the
resolve function the caller supplies, which is what lets the loader
answer with the value a name is about to have rather than the one it has
now.
Functions
expand()
env.expand.expand(value: string, resolve: function) -> string
Replaces every $ reference in value with what resolve answers, and
returns the result.
resolve is called with one variable name and returns its value, or
nil when the name has none. Everything the caller wants to decide
about precedence, recursion and cycles is decided inside it.
The forms understood are $NAME, ${NAME}, and ${NAME} carrying one
of three modifiers:
| Written | Means |
|---|---|
${NAME:-fallback} | the value, or fallback when it is unset or empty |
${NAME-fallback} | the value, or fallback when it is unset |
${NAME:+instead} | instead when the value is set and not empty, otherwise nothing |
${NAME+instead} | instead when the value is set, otherwise nothing |
${NAME:?reason} | the value, or a MissingVariable carrying reason |
${NAME?reason} | the same, counting an empty value as set |
The text after a modifier is itself expanded, so
${HOST:-${FALLBACK_HOST}} does what it looks like. A $ that names
nothing stands for itself, and \$ is always a literal dollar sign.
Parameters
value(string)resolve(function)
Returns string
Raises EnvError when a reference is not closed or carries a
modifier that does not exist.
Raises MissingVariable when a ${NAME:?reason} reference finds
nothing.
2026, Richard Ore and Zuri contributors
env.loading
import env.loading
envlifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledenv.loading.*needsimport env.loading.
The loader: which sources to read, in what order, and what to do with the names that come out of them.
A load is three steps that stay separate. The sources are scanned into
names and values, the $ references among them are resolved, and then
the result is written into the process environment. Calling read()
instead of load() stops after the second, which is what makes a .env
file inspectable without anything being set.
Classes
Result
class env.Result
What a load did.
values is everything the sources defined, after expansion, in the
order the names were first seen. The three lists beside it account for
each name and each file, so a program never has to guess whether its
configuration actually arrived.
var result = env.load()
if result.files.is_empty() {
log.warn('no .env file was found; using the environment as it stands')
}
for name in result.skipped {
log.debug('${name} was already set, so the file did not change it')
}
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
env.Result()
Result.to_string()
env.Result.to_string()
Loader
class env.Loader
Builds a load: the sources it reads and the four decisions it makes about them.
Every setting returns the loader, so a whole configuration is one expression:
import env
var result = env.loader()
.path('.env')
.path('.env.local')
.override()
.load()
Sources are read in the order they were added and layered on top of one
another, so the last source to define a name is the one that defines it.
That is what makes the pair above work: .env holds what the team
shares and .env.local holds what one machine does differently.
A loader is reusable. Nothing about it changes when load() runs, so
the same one can be kept and run again.
Constructor
env.Loader()
Loader.path()
env.Loader.path(path: string) -> Loader
Adds a file to read.
A leading ~ is expanded to the home directory, and a relative path is
resolved against the current working directory. A file that is not there
is not an error unless required() says so.
Parameters
path(string)
Returns Loader
Loader.paths()
env.Loader.paths(paths: list) -> Loader
Adds several files to read, in the order given.
Parameters
paths(list)
Returns Loader
Loader.source()
env.Loader.source(text: string) -> Loader
Adds text to read, as though it were the contents of a file.
This is how configuration that arrived over the network, out of a secret store, or from a test fixture goes through exactly the same parsing, expansion and precedence as a file on disk.
Parameters
text(string)
Returns Loader
Loader.override()
env.Loader.override(enabled: ?bool) -> Loader
Sets whether a name already present in the process environment is replaced by the one the sources define.
Off by default, which is what makes a .env file a set of defaults:
whatever the shell, the orchestrator or the CI runner already set
survives, and the file fills in the rest. Turn it on and the file wins
instead.
A variable set to the empty string counts as unset either way.
Parameters
enabled(?bool) — Defaults totruewhen the argument is left out.
Returns Loader
Loader.expand()
env.Loader.expand(enabled: ?bool) -> Loader
Sets whether $ references inside values are resolved.
On by default. Turn it off and every value is used exactly as it was written, which is what a file of opaque secrets wants.
Parameters
enabled(?bool) — Defaults totruewhen the argument is left out.
Returns Loader
Loader.required()
env.Loader.required(enabled: ?bool) -> Loader
Sets whether a file that is not there is an error.
Off by default, because the same program usually runs both on a laptop
with a .env file and on a server where the orchestrator supplies the
environment directly. Turn it on where the file is genuinely part of the
deployment and its absence should stop the program rather than surface
later as a missing setting.
Parameters
enabled(?bool) — Defaults totruewhen the argument is left out.
Returns Loader
Raises MissingFile from load() and read() when a file is
absent.
Loader.read()
env.Loader.read() -> Result
Reads the sources and returns what they hold, without touching the process environment.
$ references still resolve, and they resolve the same way they would
during a real load, so what comes back is what load() would have set.
The applied and skipped lists on the result are empty, because
nothing was applied and nothing was skipped.
Returns Result
Raises ParseError when a source is not a valid environment file.
Raises MissingFile when required() is on and a file is absent.
Loader.load()
env.Loader.load() -> Result
Reads the sources and writes what they hold into the process environment.
A name already set to a non-empty value is left alone and recorded in
Result.skipped, unless override() is on. Everything else is set and
recorded in Result.applied.
import env
env.loader().path('.env').required().load()
Returns Result
Raises ParseError when a source is not a valid environment file.
Raises MissingFile when required() is on and a file is absent.
Raises MissingVariable when a ${NAME:?reason} reference finds
nothing.
2026, Richard Ore and Zuri contributors
env.parser
import env.parser
envlifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledenv.parser.*needsimport env.parser.
The scanner that turns the text of an environment file into names and values, and the writer that turns them back into text.
The grammar is small enough to state in full. A source is a sequence of lines. A line is blank, a comment, or an assignment:
assignment := 'export'? NAME '=' value?
NAME := [A-Za-z_][A-Za-z0-9_]*
value := bare | '...' | "..." | `...`
Everything interesting is in what each kind of value means, and that is
documented on parse() where people will look for it.
Functions
is_name()
env.is_name(name: string) -> bool
Returns true when name is a legal environment variable name.
That means a letter or underscore, then any number of letters, digits
and underscores. It is the same rule POSIX shells apply, and the same
one the parser and stringify() enforce.
Parameters
name(string)
Returns bool
parse()
env.parse(source: string|bytes) -> dict
Reads the text of an environment file and returns its names and values.
This is the syntax layer on its own: no $ reference is resolved and
nothing is written to the process environment. load() is what does
both.
import env
var values = env.parse('PORT=8080\nGREETING="hello there"')
echo values.PORT
echo values.GREETING
8080
hello there
The rules
A name is a letter or underscore followed by letters, digits and
underscores. Anything else is a ParseError rather than a line quietly
dropped, because a name a shell could never export is a typo every time.
Blank lines are skipped, and so is everything from a # to the end of
the line. Inside an unquoted value a # only opens a comment when it
starts the value or follows a space, so KEY=db#1 keeps its #.
A leading export is allowed and ignored, so the same file can be fed
to source in a shell.
Values come in four forms:
| Written | Means |
|---|---|
KEY=value | the text up to a comment or the end of the line, trimmed at both ends |
KEY='value' | every character exactly as written, newlines included |
KEY=`value` | the same as single quotes, for values that contain both kinds |
KEY="value" | backslash escapes resolved, $ references left for expansion |
KEY= is an empty value, and an empty value counts as unconfigured
everywhere else in this module.
Inside double quotes, \n, \r, \t, \f, \v, \b, \a, \e and
\0 mean what they do in Zuri, \xHH, \uHHHH and \u{H...} name a
codepoint, a backslash before a newline joins the two lines, and \$ is
a literal $ that expansion will not touch. A backslash before anything
else keeps both characters, so "C:\Users" survives intact.
Line endings are normalised to \n, and a byte-order mark at the head
of the file is discarded.
Duplicates
The last assignment to a name in a source wins, which is what makes commenting out the line above the one you want work.
Parameters
source(string|bytes)
Returns dict
Raises ParseError when the source is not a valid environment file.
stringify()
env.stringify(values: dict) -> string
Turns a dictionary of names and values into the text of an environment file.
The output round-trips: feeding it back to parse() returns the same
names and the same values. Values are written bare where that is
unambiguous and double-quoted where it is not, and a $ inside a value
is escaped so that loading the file back does not expand it.
import env
print(env.stringify({
PORT: 8080,
GREETING: 'hello there',
DEBUG: false,
}))
PORT=8080
GREETING="hello there"
DEBUG=false
Names are written in the order the dictionary holds them. A number, a
bigint, a boolean or a bytes value is converted to its text; any other
type is a TypeError, because guessing what a list should look like in
an environment file is how configuration goes wrong silently.
Parameters
values(dict)
Returns string
Raises ValueError when a key is not a legal variable name.
Raises TypeError when a value is of a type that has no spelling
here.
2026, Richard Ore and Zuri contributors
env.values
import env.values
envlifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledenv.values.*needsimport env.values.
Reading configuration back out of the process environment, in the type the program actually wants.
An environment variable is always text. A port is a number, a feature
flag is a boolean, and an allow-list is a list, so somewhere that text
has to be checked and converted. Doing it here means it is done the same
way everywhere, and that a PORT=eighty is a loud error at startup
rather than a silent zero halfway through the first request.
Empty means unconfigured
Every function here treats a variable set to the empty string exactly as
it treats one that was never set at all: the default applies, and
require() raises. This is the same rule ${NAME:-...} follows, and it
is what makes a FLAG= line, an unset shell variable and an
orchestrator passing an empty value all mean one thing instead of three.
os.get_env() is the unfiltered view for the rare case that needs to
tell those apart.
Functions
has()
env.has(name: string) -> bool
Returns true when name is set to a non-empty value.
if env.has('SENTRY_DSN') {
reporting.enable(env.require('SENTRY_DSN'))
}
Parameters
name(string)
Returns bool
get()
env.get(name: string, default_value) -> ?any
Returns the value of name, or default_value when it is unset or
empty.
var host = env.get('HOST', '127.0.0.1')
Parameters
name(string)default_value(?any) — Returned unchanged;nilwhen not given.
Returns ?any
require()
env.require(name: string, message: ?string) -> string
Returns the value of name, and raises when it is unset or empty.
This is the line to write for a setting the program cannot invent a default for. Failing here, at startup, beats failing later at the first request that needed it.
var secret = env.require('SESSION_SECRET')
Parameters
name(string)message(?string) — A reason to report instead of the default text.
Returns string
Raises MissingVariable when name is unset or empty.
int()
env.int(name: string, default_value: ?number) -> ?number
Returns the value of name as an integer.
The value must be a decimal integer, optionally signed. Anything else is
a ValueError naming the variable, because a port of eighty is a
mistake in the configuration and not a reason to fall back to a default.
var port = env.int('PORT', 8080)
Parameters
name(string)default_value(?number) — Returned whennameis unset or empty.
Returns ?number
Raises ValueError when the value is not a decimal integer.
float()
env.float(name: string, default_value: ?number) -> ?number
Returns the value of name as a number.
Decimals and exponents are both accepted. Anything else is a
ValueError naming the variable.
var timeout = env.float('REQUEST_TIMEOUT', 2.5)
Parameters
name(string)default_value(?number) — Returned whennameis unset or empty.
Returns ?number
Raises ValueError when the value is not a number.
bool()
env.bool(name: string, default_value: ?bool) -> ?bool
Returns the value of name as a boolean.
1, true, yes, y and on are true; 0, false, no, n and
off are false. Case and surrounding whitespace do not matter. Anything
else is a ValueError, so a DEBUG=maybe is caught rather than quietly
read as true.
if env.bool('DEBUG', false) {
log.set_level(log.Debug)
}
Parameters
name(string)default_value(?bool) — Returned whennameis unset or empty.
Returns ?bool
Raises ValueError when the value is not one of the spellings
above.
list()
env.list(name: string, separator: ?string, default_value: ?list) -> ?list
Returns the value of name split into a list.
The value is split on separator, each item loses the whitespace around
it, and empty items are dropped, so A, B,,C and A,B,C give the same
three items.
var origins = env.list('CORS_ORIGINS', ',', [])
Parameters
name(string)separator(?string) — Defaults to','.default_value(?list) — Returned whennameis unset or empty.
Returns ?list
2026, Richard Ore and Zuri contributors
io
import io
This module provides interfaces for working with to I/O stream and TTYs as well as expose the operating system standard I/O for easy access.
Some I/O operations that should belong to this module have been merged
as core features and offered as built-in functions for Zuri.
Specifically file I/O features that can be accessed via the built-in
file() function.
The standard I/O streams are also files and you can call almost all file methods on them. Whenever a file method is not supported, you’ll get an error message telling you that such operation is not supported for standard streams.
Example
The following example shows how to use the io module for accepting
user name and printing the result.
import io
var name = io.readline('What is your name?')
echo name
The io API
Every public name in io, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
io.BytesIO | class | The BytesIO class implements a bytearray based I/O system that allows you use treat bytearray (bytes) as if… |
io.SEEK_CUR | constant | Set I/O position from the current position. |
io.SEEK_END | constant | Set I/O position from the end. |
io.SEEK_SET | constant | Set I/O position from the beginning. |
io.TTY | class | class TTY is an interface to TTY terminals this class contains definitions to control TTY terminals |
io.capture | function | Runs body with standard output captured, and returns everything it printed. |
io.capture_begin | function | Begins capturing standard output. |
io.capture_depth | function | The number of capture frames currently open. |
io.capture_end | function | Ends the innermost capture and returns everything it collected. |
io.flush | function | Flushes the content of the given file handle |
io.getc | function | Reads character(s) from standard input. |
io.getch | function | Reads a single character from standard input without printing to standard output. |
io.is_repl | constant | Returns true if the current environment is the Zuri REPL, false otherwise. |
io.putc | function | Writes character c to the screen. |
io.readline | function | Reads an entire line from standard input. |
io.stderr | constant | Stderr is a file handle to the standard error file of the system. |
io.stdin | constant | Stdin is a file handle to the standard input file of the system. |
io.stdout | constant | Stdout is a file handle to the standard output file of the system. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
io.bytesio | io.bytesio.* | BytesIO: an in-memory buffer that behaves like a file. |
io.tty | io.tty.* | TTY: control over a terminal attached to a file handle. |
Constants
SEEK_SET
io.SEEK_SET: int = 0
Set I/O position from the beginning.
SEEK_CUR
io.SEEK_CUR: int = 1
Set I/O position from the current position.
SEEK_END
io.SEEK_END: int = 2
Set I/O position from the end.
stdin
io.stdin: file
Stdin is a file handle to the standard input file of the system.
It is opened in binary mode ('rb'), so stdin.read() and
stdin.gets() return bytes, never a string. Whatever is piped into a
program is arbitrary data, and decoding it as text before the program
has asked for text can only lose information.
Call .to_string() on the result when the input really is text:
import io
var raw = io.stdin.read() # bytes
var text = raw.to_string() # decoded, replacing bad bytes
to_string() substitutes U+FFFD for undecodable bytes rather than
raising. To reject malformed input instead, inspect the bytes yourself
before decoding.
stdout
io.stdout: file
Stdout is a file handle to the standard output file of the system.
Writes accept either a string or bytes, so unlike stdin there is no
text/binary distinction to make here.
stderr
io.stderr: file
Stderr is a file handle to the standard error file of the system.
is_repl
io.is_repl: bool
Returns true if the current environment is the Zuri REPL, false
otherwise.
Functions
capture_begin()
io.capture_begin()
Begins capturing standard output.
From this call until the matching capture_end(), everything Zuri
writes to standard output is collected into a buffer instead of reaching
the terminal. That covers echo, print(), and writes through
io.stdout. It does not cover io.stderr, output written by a child
process started with os.spawn(), or bytes a native module writes
straight to the descriptor.
Captures nest. Each capture_begin() opens a new frame and the
innermost open frame receives everything, so an inner capture never
leaks into the one around it.
Capture is per-thread: one opened inside an isolate collects only that isolate’s own output.
A frame that is never closed is flushed to the terminal when the program
ends, so output is never silently lost. Prefer capture() unless you
need the output of a body that raises.
import io
io.capture_begin()
echo 'not printed yet'
var text = io.capture_end()
echo 'captured: ' + text.trim('\n')
captured: not printed yet
capture_end()
io.capture_end()
Ends the innermost capture and returns everything it collected.
The text comes back exactly as written, trailing newline included. Bytes that are not valid UTF-8 decode to U+FFFD rather than raising, so a whole frame is never lost over one stray byte.
Returns — string: the captured output, or nil when no capture was
open.
capture_depth()
io.capture_depth() -> int
The number of capture frames currently open. 0 means output is going
to the terminal.
Returns int
capture()
io.capture(body: function) -> string
Runs body with standard output captured, and returns everything it
printed.
import io
def greet(name) {
echo 'Hello, ${name}!'
}
var out = io.capture(@{ greet('Ada') })
echo out.length()
12
Parameters
body(function)
Returns string
Note: when
bodyraises, the capture is closed and the error is re-raised, but whatever was collected before it is discarded. Usecapture_begin()/capture_end()around your owncatchwhen you need both.
flush()
io.flush(file)
Flushes the content of the given file handle
putc()
io.putc(c)
Writes character c to the screen.
Parameters
char|number— c
getc()
io.getc(length)
Reads character(s) from standard input.
When length is given, gets length number of characters else, gets a
single character.
length counts bytes, not codepoints, so asking for fewer bytes than a
multi-byte character occupies yields a replacement character rather than
half of one.
Returns — char|string
getch()
io.getch()
Reads a single character from standard input without printing to standard output.
Returns — char|string
readline()
io.readline(message, secure, obscure_text) -> string
Reads an entire line from standard input. If a message is given, the
message will be printed before it begins to wait for a user input. If
secure is true, the user’s input will not be printing and
obscure_text will be printed instead.
Parameters
message(?string)secure(?bool)obscure_text(?string) — Default value is*.
Returns string
Note: Newlines will not be added automatically for messages.
2021, Richard Ore and Zuri contributors
io.bytesio
import io.bytesio
iolifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledio.bytesio.*needsimport io.bytesio.
BytesIO: an in-memory buffer that behaves like a file.
Anything that reads or writes through a file handle can be pointed at one of these instead, which is what makes a function that writes to disk testable without touching the disk.
Classes
BytesIO
class io.BytesIO
The BytesIO class implements a bytearray based I/O system that allows you use treat bytearray (bytes) as if they were a file.
The class implements the essentials of a file except those that ties it to the operating system filesystem such as symbolic links, chmod and set time.
See the chapter on files in The Zuri Programming Language for more information.
Constructor
io.BytesIO(source, mode)
Returns a new instance of BytesIO
Parameters
source(bytes)mode(string) — The I/O open mode - Default isr
BytesIO.exists()
io.BytesIO.exists() -> bool
Returns true as BytesIO always exist.
Returns bool
BytesIO.close()
io.BytesIO.close()
Closes the stream to an opened BytesIO. You’ll rarely ever need to call this method yourself in most use cases.
BytesIO.open()
io.BytesIO.open() -> bool
Opens the stream to a BytesIO for the operation originally specified on the BytesIO object during creation.
You may need to call this method after a call to read() if the length
isn’t specified or write() if you wish to read or write again as the
BytesIO will already be closed.
Returns bool
BytesIO.read()
io.BytesIO.read(length) -> bytes
Reads the content of an opened BytesIO up to the specified length and returns it as string or bytes if the BytesIO was opened in the binary mode. If the length is not specified, the BytesIO will be read to the end.
This method requires that the BytesIO be opened in the read mode (default mode) or a mode that supports reading. If you aren’t reading the full length of the BytesIO, you’ll need to call the close() method to free the BytesIO for further reading, otherwise, the close() method will be automatically called for you.
Parameters
length(number) — Default = -1
Returns bytes
Raises Error
BytesIO.gets()
io.BytesIO.gets(length) -> bytes
Same as read(), but doesn’t open or close the BytesIO automatically.
Parameters
length(number) — Default = -1
Returns bytes
Raises Error
BytesIO.write()
io.BytesIO.write(data) -> number
Writes a string or bytes to an opened BytesIO at the current insertion point. When the BytesIO is opened with the a mode enabled, write will always start from the end of the BytesIO.
If the seek() method has been previously called, write will begin from the seeked position, otherwise it will start at the beginning of the BytesIO.
Parameters
- `` (
bytes|string)
Returns number
BytesIO.puts()
io.BytesIO.puts(data) -> number
Same as write(), but doesn’t open or close the BytesIO automatically.
Parameters
- `` (
bytes|string)
Returns number
BytesIO.number()
io.BytesIO.number() -> number
Returns the integer file descriptor number that is used by the underlying implementation to request I/O operations from the operating system. This can be very useful for low-level interfaces that uses or act as BytesIO descriptors.
Returns number
BytesIO.is_tty()
io.BytesIO.is_tty() -> bool
Always returns false as a BytesIO is not a TTY device.
Returns bool
BytesIO.is_open()
io.BytesIO.is_open() -> bool
Returns true if the BytesIO is open for reading or writing and false
otherwise.
Returns bool
BytesIO.is_closed()
io.BytesIO.is_closed()
Returns true if the BytesIO is closed for reading or writing and
false otherwise.
BytesIO.flush()
io.BytesIO.flush()
Does nothing for a BytesIO
BytesIO.symlink()
io.BytesIO.symlink(path) -> bool
Does nothing for BytesIO but simply returns false because BytesIO
cannot be symbolically linked.
Returns bool
BytesIO.stats()
io.BytesIO.stats() -> dict
Returns the statistics or details of the BytesIO.
See the working with files documentation for more information about the
stats() method.
Returns dict
BytesIO.delete()
io.BytesIO.delete() -> bool
Clears the bytearray and closes it for reading or writing.
Any further attempt to perform most operations on the BytesIO after
calling delete() will raise an error.
Returns bool
BytesIO.rename()
io.BytesIO.rename(new_name) -> bool
Returns false because BytesIO cannot be renamed.
Returns bool
BytesIO.copy()
io.BytesIO.copy() -> [[io.BytesIO]]
Returns a new BytesIO with the source cloned and opened with the same mode as the current BytesIO.
Returns [[io.BytesIO]]
BytesIO.path()
io.BytesIO.path() -> string
Returns an empty string because BytesIO do not have any physical path.
Returns string
BytesIO.abs_path()
io.BytesIO.abs_path() -> string
Same as [[io.BytesIO.path()]].
Returns string
BytesIO.truncate()
io.BytesIO.truncate(length) -> bool
Truncates the entire BytesIO if length is not given or truncates the BytesIO such that only length number of bytes is left in it.
Returns bool
BytesIO.chmod()
io.BytesIO.chmod(number) -> bool
Returns false because BytesIO do not have a permission scheme.
Returns bool
BytesIO.set_times()
io.BytesIO.set_times(atime, mtime) -> bool
Sets the last access time and last modified time of the BytesIO.
Returns bool
BytesIO.seek()
io.BytesIO.seek(position, seek_type) -> bool
Sets the position of a BytesIO reader or writer in a BytesIO.
The position must be within the range of the BytesIO size. The
seek_type argument must be on of [[io.SEEK_SET]], [[io.SEEK_CUR]] or
[[io.SEEK_END]].
Returns bool
BytesIO.tell()
io.BytesIO.tell() -> number
Returns the current position of the reader/writer in the BytesIO.
Returns number
BytesIO.mode()
io.BytesIO.mode() -> string
Returns the mode in which the current BytesIO was opened.
Returns string
BytesIO.name()
io.BytesIO.name() -> string
Returns an empty string since BytesIO do not have a name.
Returns string
2026, Richard Ore and Zuri contributors
io.tty
import io.tty
iolifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledio.tty.*needsimport io.tty.
TTY: control over a terminal attached to a file handle.
Raw and cooked modes, echo, cursor visibility and terminal size all live
here. The underlying constants are read from the platform’s own libc
at load time rather than hard-coded, because they differ between
operating systems.
Classes
TTY
class io.TTY
class TTY is an interface to TTY terminals this class contains definitions to control TTY terminals
Termios flag words are a Unix concept, so get_attr() and set_attr()
raise on Windows and every flag constant below reads as 0 there. What
those two are usually wanted for is not Unix-only, though: set_raw()
and exit_raw() work on both, Windows reaching its console mode
directly instead of through the flags.
exit_raw() and flush() never raise on any platform. Each reports
whether it had anything to do, so cleanup code can call them without
first establishing what state the terminal was in.
Constructor
io.TTY(std)
Parameters
std(file)
Note: file must be one of stdout and stderr
TTY.get_attr()
io.TTY.get_attr() -> dict
Returns the attributes of the current tty session. The returned value is a dict keyed by the TTY_ group constants.
Returns dict
Raises Error on a platform without termios, or if the stream has
no terminal behind it.
Note: Unix only. Termios flag words have no console equivalent, so this raises on Windows.
set_raw()andexit_raw()cover what attributes are usually reached for and work on both.
TTY.set_attr()
io.TTY.set_attr(option: int, attrs: dict) -> bool
sets the attributes of the current tty session
NOTE: - option must be one ot the TCSA options above (see their description above) - attrs must be a dictionary keyed by the TTY_ group constants above (TTY_IFLAG, TTY_OFLAG, TTY_CFLAG, TTY_LFLAG, TTY_ISPEED, TTY_OSPEED hold a single bit-flag number each; TTY_CC holds a list of control character values, indexed by the VEOF/VERASE/etc constants above) - one can safely omit any of the TTY_ groups listed above and Zuri will fill in the default values as it exists.
- This flags will be merged and not overwritten
Parameters
option(number)attr(dict)
Returns bool
Raises Error on a platform without termios, or if the stream has
no terminal behind it.
Note: Unix only, for the same reason
get_attr()is: there is no console representation of a termios flag word to apply, and accepting one would mean reporting success for a change that never happened.
TTY.set_raw()
io.TTY.set_raw() -> bool
Sets the current tty to raw mode: input arrives a keystroke at a time, nothing is echoed back, and Ctrl+C reaches the program as a keystroke rather than being turned into a signal first.
Works on Windows as well as Unix.
Returns bool
Raises Error if the stream has no terminal behind it, which is
what a redirected or captured stream is.
TTY.exit_raw()
io.TTY.exit_raw() -> bool
Disables the raw mode flags on the current tty, putting it back the way
set_raw() found it.
Never raises, so it is safe to call on a cleanup path without knowing
whether raw mode was ever entered. The return value says whether
anything was actually restored: false for a stream this TTY never put
into raw mode, and false on Windows, which has no raw mode to leave.
Returns bool
TTY.get_size()
io.TTY.get_size() -> dict
Returns the size of the current TTY device as a dictionary of cols and rows.
cols: the number of text columns that fit into the TTY device.rows: the number of text rows that fit into the TTY device.
Works on Windows as well as Unix.
Returns dict
Raises Error if the stream has no terminal behind it, which is
what a redirected or captured stream is.
TTY.flush()
io.TTY.flush() -> bool
Discards whatever is still queued on this TTY’s stream, both input that has been typed but not yet read and output that has been written but not yet sent.
Note that this throws that data away rather than writing it out, which
is the opposite of what file.flush() does.
Never raises. The return value says whether anything was discarded:
false for a stream with no terminal behind it, and false on Windows,
which keeps no such queue.
Returns bool
2026, Richard Ore and Zuri contributors
args
import args
This module provides a complete, batteries-included framework for
building command-line interfaces. It supports options (flags),
positional arguments, sub-commands with their own option sets, automatic
help generation, type coercion, required arguments, deprecation
warnings, choice validation, abbreviated long-option matching, --
end-of-options, and @file argument expansion.
How a value reaches an option
A long option takes its value either as the next argument or attached
with =; both spellings mean the same thing:
$ zuri run myprogram.zu --name Alice
$ zuri run myprogram.zu --name=Alice
The split is on the first =, so --path=a=b sets path to a=b. The
attached form is the only way to pass a value that begins with a dash,
since --name -5 reads -5 as an option:
$ zuri run myprogram.zu --offset=-5
A short option takes its value as the next argument only. -n Alice
works; -n=Alice does not, because a short token is a bundle of
single-character flags and = is not one of them. Every character in
such a bundle has to name an option, so a typo in -vq is an error
rather than a silently dropped flag.
Quick start
import args
var parser = args.Parser('myprogram')
parser.description = 'A friendly CLI tool.'
parser.add_option('name', 'Person to greet', {short_name: 'n', type: args.STRING})
parser.add_option('count', 'Number of greetings', {short_name: 'c', type: args.INT, value: 1})
var cmd = parser.add_command('call', 'Make a phone call')
cmd.add_option('verbose', 'Enable verbose output', {short_name: 'v'})
parser.parse()
Running the following command:
zuri run myprogram.zu -h
Prints the following help output:
Usage: myprogram [OPTIONS] [COMMAND]
A friendly CLI tool.
OPTIONS:
-h, --help Show this help message and exit
-n, --name <name> Person to greet
-c, --count <count> Number of greetings (default: 1)
COMMANDS:
call Make a phone call
Run "myprogram --help [COMMAND]" for help on a specific command.
Typical invocations:
$ zuri run myprogram.zu -h
$ zuri run myprogram.zu --name Alice --count 3
$ zuri run myprogram.zu call --verbose
$ zuri run myprogram.zu call --help
If we change the last line of the program to echo parser.parse() so
that we can see the result of the parsing, the following CLI call will
yield the given result.
$ zuri run myprogram.zu --name "Kirk"
{options: {name: Kirk, count: 1}, command: nil, indexes: []}
$ zuri run myprogram.zu call
{options: {count: 1}, command: {name: call, value: nil}, indexes: []}
$ zuri run myprogram.zu call -v
{options: {verbose: true, count: 1}, command: {name: call, value: nil}, indexes: []}
Calling name without an option will yield the following result/error:
$ zuri run myprogram.zu --name
error: option --name expects <name>
You may even get help on a command directly like below:
$ zuri run myprogram.zu --help call
Usage: myprogram call [OPTIONS]
Make a phone call
OPTIONS:
-v, --verbose Enable verbose output
GLOBAL OPTIONS:
-h, --help Show this help message and exit
-n, --name <name> Person to greet
-c, --count <count> Number of greetings (default: 1)
Options declared on the parser itself are global: they are accepted before a command and after it alike, and a command’s help lists them under GLOBAL OPTIONS. When a command declares an option of the same name, that option is the one its arguments reach.
Return value of parse() is in the format:
{
options: {name: 'Alice', count: 3},
command: {name: 'call', value: nil},
indexes: []
}
The args API
Every public name in args, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
args.ArgsError | class | Error raised for argument parsing errors. |
args.BOOL | constant | value type boolean (accepts 1/0, true/false, yes/no, on/off). |
args.CHOICE | constant | value type choice: value must be one of the choices list/dict |
args.INT | constant | value type integer (accepts numbers, floors to integer) |
args.LIST | constant | value type list: the option may be supplied multiple times |
args.NONE | constant | value type none: the option is a boolean flag |
args.NUMBER | constant | value type number |
args.OPTIONAL | constant | value type optional: value is consumed if the next token is not a flag |
args.Parser | class | A configurable command-line parser. |
args.STRING | constant | value type string |
Constants
NONE
args.NONE = 0
value type none: the option is a boolean flag
INT
args.INT = 1
value type integer (accepts numbers, floors to integer)
NUMBER
args.NUMBER = 2
value type number
BOOL
args.BOOL = 3
value type boolean (accepts 1/0, true/false, yes/no,
on/off).
STRING
args.STRING = 4
value type string
LIST
args.LIST = 5
value type list: the option may be supplied multiple times
CHOICE
args.CHOICE = 6
value type choice: value must be one of the choices list/dict
OPTIONAL
args.OPTIONAL = 7
value type optional: value is consumed if the next token is not a flag
Classes
ArgsError
class args.ArgsError < Error
Error raised for argument parsing errors.
Parser
class args.Parser < _Optionable
A configurable command-line parser.
Properties you can set after construction
description(string): paragraph shown between USAGE and OPTIONS.epilog(string): paragraph shown after all sections.allow_abbrev(bool): allow unambiguous long-option prefix matching. Defaults totrue, like Python’s argparse.allow_atfile(bool): expand@filenametokens by reading arguments from that file. Defaults totrue.
Fields
| Field | Type | Description |
|---|---|---|
commands | List of sub-commands registered with add_command. | |
indexes | List of positional arguments registered with add_index. | |
description | ||
epilog | ||
allow_abbrev | ||
allow_atfile | ||
terminal_width | The column width help text wraps to. |
Constructor
args.Parser(name, default_help)
Creates a new parser instance.
Parameters
name(string) — The program name shown in usage lines.default_help(?bool) — Show help when invoked with no arguments. Defaults totrue.
Parser.set_terminal_width()
args.Parser.set_terminal_width(width)
Overrides the column width help text wraps to, in place of the
terminal_width this parser auto-detected at construction time (a real
query of the terminal, the COLUMNS environment variable, or 80, in
that order of preference; see _terminal_width()). Useful both for a
program that wants a fixed layout regardless of environment, and for
tests that need deterministic wrapping independent of whatever terminal
actually ran them.
Equivalent to setting terminal_width directly; this exists purely for
discoverability and to validate its argument.
Parameters
width(number)
Parser.add_option()
args.Parser.add_option(name, help, opts)
Adds an option (flag) to the top-level parser.
opts keys can include any of:
short_name(string): single-character alias (-x).type(int): one of the type constants; defaultNONE.value(any): default value when the option is absent.choices(list|dict): restrict allowed values.required(bool): error if the option is absent.metavar(string): placeholder shown in help, defaulting to the option’s own name, lower-cased.deprecated(bool): print a warning when the option is used.
Parameters
name(string)help(?string)opts(?dict)
Parser.add_command()
args.Parser.add_command(name, help, opts)
Adds a sub-command.
opts keys: - type {int}: expected type for the command’s value
argument - action {function}: called with (options [, value]) after
parsing - choices {list|dict}— restrict allowed values when type is
CHOICE - metavar {string}: placeholder shown in help for the
command’s value, e.g. 'message' for git commit -m <message>
(default: the value type’s name, lower-cased)
Returns the _Command object so you can chain add_option calls:
parser.add_command('push', 'Push changes').
add_option('force', 'Force push', {short_name: 'f'})
Parameters
name(string)help(?string)opts(?dict)
Returns — _Command
Parser.add_index()
args.Parser.add_index(name, help, opts)
Adds a positional (index-based) argument.
opts keys: - type {int}: coercion type; default STRING - value
{any}: default when argument is absent - choices {list|dict}: restrict
allowed values - required {bool}: error if argument is absent (default
false) - metavar {string}: display name in help
Without a value, a positional nobody supplied is simply missing from
parse()’s indexes rather than sitting there as nil.
A positional of type LIST takes every positional word from where it
starts to the end of the command line, flags in between or not, and
arrives in indexes as one list. It has to be the last positional
declared, since nothing after it could ever receive a word.
Parameters
name(string)help(?string)opts(?dict)
Raises ArgsError when a positional is added after a LIST one.
Parser.parse()
args.Parser.parse(custom_args: ?list) -> dict
Parses command-line arguments and returns a dictionary of command, options, and indexes.
By default this reads the real process arguments (os.args, skipping
the interpreter and script path). Pass custom_args (a list of strings)
to parse something else instead, e.g. a config-driven argument list, or
a fixed list in a test.
Result shape:
{
options: dict, # collected option values
command: nil | {name, value}, # command name and value (if any)
indexes: list # collected positional values
}
indexes holds one entry per positional that has a value, in the order
they were declared: what the user supplied, then the declared default of
any that follow. A positional that was not supplied and has no default
is absent rather than nil, so indexes.is_empty() and
indexes.length() answer what they look like they answer, and a program
that wants its own fallback can reach for it:
var path = parsed.indexes.is_empty() ? os.cwd() : parsed.indexes[0]
Parameters
custom_args(?list)
Returns dict
Parser.help()
args.Parser.help()
Print the full help text and exit(0).
2021, Richard Ore and Zuri contributors
log
import log
This module implements a simple and flexible event logging system for all Zuri applications and modules. With support for multiple transport systems as well as custom transports, this module allows easy application logging and log shipping.
The module selects defaults that is familiar for most end use-cases in order to allow for a minimal need for configurations so that you can start logging right out of the box.
Below is a very simple but powerful and complete usage of this module:
%> import log
%> log.info('Starting my application...')
'2025-03-07T08:00:33+01:00 INFO [.]: Starting my application...'
IMPORTANT!
Did you notice that
[.]? That’s because we are running in a REPL. By default, thelogmodule provides information regarding the source of the log i.e. the application from which the log came from thereby allowing multiple applications log into the same transport pool without ambiguity.You can customize this name by setting the name on the transport via [[log.Transport.set_name]].
This module provides all functionalities at the module level, allowing configurations to be carried across all files and modules in the lifetime of an application.
While allowing creation of custom transports for logs, the module provides transports for logging to the console ([[log.ConsoleTransport]]) and files ([[log.FileTransport]]) out of the box. This covers the most simple use-cases for most applications.
The default transport enabled is the [[log.ConsoleTransport]] known as the [[log.default_transport()]] and need no extra work to enable unless you have previously disabled it. The example below shows how to enable the file transport to log to a file on disk.
import log
var transport = log.FileTransport('mylog.log')
log.add_transport(transport)
log.info('Finished setting up file log...')
If you check the file mylog.log now, you should see something like
this:
2025-03-01T12:00:00+01:00 INFO [tmp]: Finished setting up file log...
In addition to the log appearing on the console, you can now see the log in a persistent file. There are many ways to turn off the console output and log to file alone.
Firstly, you can simple disable the default transport.
log.default_transport().disable()
The advantage to this approach is that while it disables the default transport, the transport is still registered and you can simple enable it at any time during the lifetime of the application by doing the reverse:
log.default_transport().enable()
The same strategy applies to all transports as the enable() and
disable() method will be inherited from the [[log.Transport]] class.
The second approach is to completely remove the transport from the list of registered transports.
log.remove_transport(log.default_transport())
With this second approach, you’ll need to register the transport again should you want to continue logging to the console. The same applies to all transport types.
For more complex uses, the process of creating a custom transport is
really simple. To create a custom transport, you’ll need to create a
class that inherits from [[log.Transport]] and implement the write()
method at a minimum.
The example below shows the creation of a custom transport that outputs structured JSON data to the console.
# my_custom_transport.zu
import log { Transport }
import json
import enum
class JsonConsoleTransport < Transport {
format(records, level, context) {
return {
records,
level,
}
}
write(message, level) {
echo json.encode({
logtime: time(),
records: message.records,
name: self.get_name(),
level: enum.to_value_dict(log.LogLevel)[message.level]
})
}
}
import log
import .my_custom_transport { JsonConsoleTransport }
log.add_transport(JsonConsoleTransport())
log.info('Finished setting up json console log transport...')
You should be seeing something similar to the below if you run the code:
{"logtime":1741376459,"records":["Finished setting up json console log transport..."],"name":"tmp","level":"Info"}
You can set multiple transports at the same time as well as set them to only work at different log levels. Every transport inherits the method [[log.Transport.set_level]] which allows us set the minimum level at which a transport is available.
There is also a global [[log.set_level]] function that allows us to set the minimum log level at which all transports can start logging.
import log
log.set_level(log.Warning)
log.info('Finished setting up json console log transport...')
If you try the above code, you won’t be seeing anything in the console. This is because level [[log.Info]] is lower than the minium required [[log.Warning]].
While every Transport implements the format() method, users can
override the format method by providing a format function via the
[[log.Transport.set_formatter]] method. This allows using the same
formatter across different transports.
For example, in the previous example, if we had wanted to serialize all
logging irrespective of the transport into the JSON format, a more
appropriate solution would have been to create a log format function and
reuse it in all transports instead of implementing a
JsonConsoleTransport. The next example shows one such implementation.
import log
import json
import date
def json_format(records, level, transport) {
return json.encode({
time: date().format(transport.get_time_format()),
level: log.get_level_name(level),
name: transport.get_name(),
records,
})
}
var file_transport = log.FileTransport('mylog.log')
log.add_transport(file_transport)
log.default_transport().set_formatter(json_format)
file_transport.set_formatter(json_format)
log.debug('This is a debug information')
Transport implementations should take note of this critical information.
IMPORTANT!
Because the
logmodule exports the a function, if you are not interested in all the shenanigans of logging level and simply want to do some quick logging, you can ignore the whole logging levels altogether and log at level [[log.None]] by simply calling the log module itself.import log log('An anonymous log!')This bypasses every level filter (including the global [[log.set_level]] threshold) on every enabled transport; only [[log.Transport.disable]] can still silence it.
Structured and namespaced logging
A trailing dict argument on any log call is treated as structured fields
rather than a plain message, rendered as key=value pairs after the
message:
log.info('user signed in', { user_id: 42 })
# ... INFO [.]: user signed in user_id=42
For a whole subsystem that should tag every one of its own log lines with a name (and, optionally, a fixed set of fields), use [[log.get_logger]] instead of the flat module-level functions:
var db_log = log.get_logger('db').bind({ pool: 'primary' })
db_log.warn('slow query', { duration_ms: 850 })
# ... WARNING [db]: slow query pool=primary duration_ms=850
The log API
Every public name in log, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
log.ConsoleTransport | class | ConsoleTransport is a log transport that facilitates sending log streams to the console. |
log.Critical | constant | Module level declaration of LogLevel.Critical |
log.Debug | constant | Module level declaration of LogLevel.Debug |
log.Error | constant | Module level declaration of LogLevel.Error |
log.FileTransport | class | FileTransport is a log transport that facilitates sending log streams to an on-disk file. |
log.Info | constant | Module level declaration of LogLevel.Info |
log.LogLevel | constant | The Log levels in order |
log.Logger | class | A namespaced logger that tags every record it writes with a name and, optionally, a set of bound structured… |
log.None | constant | Module level declaration of LogLevel.None |
log.Transport | class | The Transport class acts as the base class for log transports and handle the actual logging of the specified… |
log.Warning | constant | Module level declaration of LogLevel.Warning |
log.add_transport | function | Adds a new transport service to the list of registered transports. |
log.critical | function | Logs a message with level [[log.Critical]] on all registered transports. |
log.debug | function | Logs a message with level [[log.Debug]] on all registered transports. |
log.default_transport | function | Returns the instance [[log.ConsoleTransport]] which is used as the default transport by the module. |
log.error | function | Logs a message with level [[log.Error]] on all registered transports. |
log.exception | function | Logs an exception with level [[log.Error]] on all registered transports, including its message and stacktrace. |
log.get_level_name | function | Returns the name of a log level as a string. |
log.get_logger | function | Returns a [[log.Logger]] namespaced under the given name. |
log.info | function | Logs a message with level [[log.Info]] on all registered transports. |
log.log | function | Logs a message with level [[log.None]] on all registered transports. |
log.remove_transport | function | Removes the given transport service from the list of registered transports. |
log.set_level | function | Sets the threshold level for the default transport to handle. |
log.set_name | function | Sets the name of the default transport. |
log.warn | function | Logs a message with level [[log.Warning]] on all registered transports. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
log.console | log.* | ConsoleTransport: the default transport, printing to stdout (or stderr for Error/Critical) with optional… |
log.dispatch | log.* | The default transport instance and the module-level functions that configure it. |
log.file | log.* | FileTransport: sends log streams to a file on disk, with optional size-based rotation. |
log.level | log.* | The LogLevel enum, its module-level constant exports, and the single shared “default level” that every… |
log.logger | log.* | The flat, module-level logging functions (log(), info(), debug(), warn(), error(), critical(),… |
log.transport | log.* | The Transport base class every log sink (console, file, or a custom subclass) is built on, plus the… |
2025, Richard Ore and The Zuri Contributors
log.console
import log
Everything here is re-exported by
log, soimport logis enough and the names are called aslog.*. Importinglog.consoleon its own works too and reaches the same definitions.
ConsoleTransport: the default transport, printing to stdout (or stderr
for Error/Critical) with optional ANSI coloring by level.
Classes
ConsoleTransport
class log.ConsoleTransport < Transport
ConsoleTransport is a log transport that facilitates sending log streams to the console.
ConsoleTransport.set_colors()
log.ConsoleTransport.set_colors(enabled: bool)
Enables or disables ANSI coloring of messages by level. Enabled by
default unless the NO_COLOR environment variable is set.
Parameters
enabled(bool)
Returns — self
ConsoleTransport.write()
log.ConsoleTransport.write(message, level)
ConsoleTransport.flush()
log.ConsoleTransport.flush()
log.dispatch
import log
Everything here is re-exported by
log, soimport logis enough and the names are called aslog.*. Importinglog.dispatchon its own works too and reaches the same definitions.
The default transport instance and the module-level functions that configure it.
Functions
set_level()
log.set_level(level)
Sets the threshold level for the default transport to handle.
Parameters
level(log.LogLevel)
Returns — default_transport
set_name()
log.set_name(name)
Sets the name of the default transport.
Parameters
name(string)
Returns — default_transport
default_transport()
log.default_transport() -> [[log.ConsoleTransport]]
Returns the instance [[log.ConsoleTransport]] which is used as the default transport by the module.
Returns [[log.ConsoleTransport]]
log.file
import log
Everything here is re-exported by
log, soimport logis enough and the names are called aslog.*. Importinglog.fileon its own works too and reaches the same definitions.
FileTransport: sends log streams to a file on disk, with optional
size-based rotation.
Classes
FileTransport
class log.FileTransport < Transport
FileTransport is a log transport that facilitates sending log streams to an on-disk file.
Fields
| Field | Type | Description |
|---|---|---|
file |
Constructor
log.FileTransport(path: string)
Returns a new instance of FileTransport and opens a file handle to the the file specified in the path.
Parameters
path(string)
FileTransport.set_max_bytes()
log.FileTransport.set_max_bytes(n: ?number)
Sets the size, in bytes, past which the log file is rotated to
<path>.1 (shifting any existing numbered backups up by one) and a
fresh file is opened at path. Rotation is disabled (nil) by default.
Parameters
n(?number)
Returns — self
FileTransport.set_backup_count()
log.FileTransport.set_backup_count(n: number)
Sets how many rotated backups (<path>.1 through <path>.<n>) are
kept. The oldest backup beyond this count is deleted on the next
rotation. Default: 5.
Parameters
n(number)
Returns — self
FileTransport.write()
log.FileTransport.write(message, level)
FileTransport.flush()
log.FileTransport.flush()
FileTransport.close()
log.FileTransport.close()
log.level
import log
Everything here is re-exported by
log, soimport logis enough and the names are called aslog.*. Importinglog.levelon its own works too and reaches the same definitions.
The LogLevel enum, its module-level constant exports, and the single
shared “default level” that every registered transport is filtered
against unless it has its own level configured.
Constants
LogLevel
log.LogLevel
The Log levels in order
- None - Debug - Info - Warning - Error - Critical
None
log.None
Module level declaration of LogLevel.None
Debug
log.Debug
Module level declaration of LogLevel.Debug
Info
log.Info
Module level declaration of LogLevel.Info
Warning
log.Warning
Module level declaration of LogLevel.Warning
Error
log.Error
Module level declaration of LogLevel.Error
Critical
log.Critical
Module level declaration of LogLevel.Critical
Functions
get_level_name()
log.get_level_name(level) -> string
Returns the name of a log level as a string.
Parameters
level(log.LogLevel)
Returns string
log.logger
import log
Everything here is re-exported by
log, soimport logis enough and the names are called aslog.*. Importinglog.loggeron its own works too and reaches the same definitions.
The flat, module-level logging functions (log(), info(), debug(),
warn(), error(), critical(), exception()), and Logger:
namespaced, structured-field-bound loggers built on top of the exact
same dispatch path.
The module is callable. log(...) is the same call as log.log(...),
documented below.
Functions
log()
log.log(...args: list)
Logs a message with level [[log.None]] on all registered transports.
The format of the output log is dependent on the specific transport service as well as limitations on the nature and type of arguments that is passed to the function.
Parameters
any...
info()
log.info(...args: list)
Logs a message with level [[log.Info]] on all registered transports. The arguments and limitations as same as in [[log.log()]].
Parameters
any...
debug()
log.debug(...args: list)
Logs a message with level [[log.Debug]] on all registered transports. The arguments and limitations as same as in [[log.log()]].
Parameters
any...
warn()
log.warn(...args: list)
Logs a message with level [[log.Warning]] on all registered transports. The arguments and limitations as same as in [[log.log()]].
Parameters
any...
error()
log.error(...args: list)
Logs a message with level [[log.Error]] on all registered transports. The arguments and limitations as same as in [[log.log()]].
Parameters
any...
critical()
log.critical(...args: list)
Logs a message with level [[log.Critical]] on all registered transports. The arguments and limitations as same as in [[log.log()]].
Parameters
any...
exception()
log.exception(ex, message)
Logs an exception with level [[log.Error]] on all registered transports, including its message and stacktrace.
Parameters
ex(any) — Expected to be an Error instance (from acatchblock); anything else is logged via its string form instead.message(?any) — Optional additional context to log alongside the exception.
get_logger()
log.get_logger(name: string) -> [[log.Logger]]
Returns a [[log.Logger]] namespaced under the given name. Repeated calls
with the same name return the same instance, matching Python’s
logging.getLogger(): so binding fields on a logger obtained one place
is visible everywhere else that same name is requested.
Parameters
name(string)
Returns [[log.Logger]]
Classes
Logger
class log.Logger
A namespaced logger that tags every record it writes with a name and, optionally, a set of bound structured fields. Get one via [[log.get_logger]] rather than constructing directly.
Example
import log
var db_log = log.get_logger('db')
db_log.info('connection established')
# ... [db]: connection established
var pool_log = db_log.child('pool').bind({ pool_size: 10 })
pool_log.warn('pool exhausted')
# ... [db.pool]: pool exhausted pool_size=10
Fields
| Field | Type | Description |
|---|---|---|
name |
Constructor
log.Logger(name: string, fields)
Logger.bind()
log.Logger.bind(fields: dict)
Logger.child()
log.Logger.child(sub_name: string)
Logger.info()
log.Logger.info(...args: list)
Logger.debug()
log.Logger.debug(...args: list)
Logger.warn()
log.Logger.warn(...args: list)
Logger.error()
log.Logger.error(...args: list)
Logger.critical()
log.Logger.critical(...args: list)
Logger.exception()
log.Logger.exception(ex, message)
log.transport
import log
Everything here is re-exported by
log, soimport logis enough and the names are called aslog.*. Importinglog.transporton its own works too and reaches the same definitions.
The Transport base class every log sink (console, file, or a custom
subclass) is built on, plus the registry of active transports and the
shared dispatch loop every level function writes through. These live in
the same file as Transport itself (rather than off in dispatch.zu
alongside default_transport()) specifically so Transport.close() can
remove itself from the registry without a cross-file call back into a
file that in turn depends on this one: console.zu/file.zu need
Transport from here, and dispatch.zu needs ConsoleTransport from
console.zu, so a Transport depending on dispatch.zu for
remove_transport() would be a genuine import cycle. Keeping the
registry here instead keeps every dependency pointing one direction.
Functions
add_transport()
log.add_transport(transport)
Adds a new transport service to the list of registered transports. If the transport has been previously added, this function will do nothing.
Parameters
transport(log.Transport)
remove_transport()
log.remove_transport(transport)
Removes the given transport service from the list of registered transports.
Parameters
transport(log.Transport)
Classes
Transport
class log.Transport
The Transport class acts as the base class for log transports and handle the actual logging of the specified log records.
Transport.set_level()
log.Transport.set_level(level)
Sets the threshold level for this transport to handle. Logging messages which are less severe than level will be ignored. Unless overridden by the transport implementation, when a handler is created, the level is set to [[log.None]] (which causes all messages to be processed).
Parameters
level(log.LogLevel)
Returns — self
Transport.get_level()
log.Transport.get_level() -> [[log.LogLevel]]
The threshold level of this transport. The default level is [[log.LogLevel.None]].
Returns [[log.LogLevel]]
Transport.set_max_level()
log.Transport.set_max_level(level)
Sets the maximum threshold level for this transport to handle. Logging messages which are more severe than level will be ignored. Unless overridden by the transport implementation, when a handler is created, the maximum level is set to [[log.Critical]] (which causes all messages to be processed).
Parameters
level(log.LogLevel)
Returns — self
Transport.get_max_level()
log.Transport.get_max_level() -> [[log.LogLevel]]
The maximum threshold level of this transport. The default maximum level is [[log.Critical]].
Returns [[log.LogLevel]]
Transport.set_name()
log.Transport.set_name(name)
Sets the name of the current transport.
Parameters
name(string)
Returns — self
Transport.get_name()
log.Transport.get_name() -> string
Returns the name of the current transport. By default, name will be equal to the name of the directory containing the root file.
Returns string
Transport.set_time_format()
log.Transport.set_time_format(format)
Sets the time formatting string used by the transport when [[log.Transport.show_time]] is set to true.
Parameters
format(string)
Returns — self
Transport.get_time_format()
log.Transport.get_time_format() -> string
Returns the time formatting string used by the current transport. The
default value is c.
Returns string
Transport.show_name()
log.Transport.show_name(show)
Enable or disable showing transport names in the logs based on the passed boolean value.
Parameters
show(bool)
Returns — self
Transport.show_time()
log.Transport.show_time(show)
Enables or disables showing logging time in the logs based on the passed boolean value.
Parameters
show(bool)
Returns — self
Transport.show_level()
log.Transport.show_level(show)
Enable or disable showing transport log level in the logs based on the passed boolean value.
Parameters
show(bool)
Returns — self
Transport.set_formatter()
log.Transport.set_formatter(formatter)
Sets a formatter function that overrides a transports format()
function.
The formatter function is a function that when it is set overrides the
transports default format method and must MUST have the contract def my_function(records, level, transport).
The transport parameter here is the instance of the current transport.
See [[log.Transport.format]] for what records, and level means.
Parameters
formatter(function)
Returns — self
Transport.get_formatter()
log.Transport.get_formatter() -> ?function
Returns the format method override that has been set for the current
transport or nil if none has been set.
Returns ?function
Transport.can_log()
log.Transport.can_log(level) -> bool
Returns a boolean value which indicates if a message of severity level can be processed by this transport.
[[log.LogLevel.None]] is a documented escape hatch that bypasses level
filtering entirely (only enable()/disable() still applies to it):
see the module doc’s “quick logging” section.
Parameters
level(log.LogLevel)
Returns bool
Transport.enable()
log.Transport.enable()
Enables and starts processing of logs by the current transport.
Returns — self
Transport.disable()
log.Transport.disable()
Disables and stops the current transport from processing further logs.
Returns — self
Transport.format()
log.Transport.format(records, level, context) -> any
Formats the log records for the current level for writing to the transport’s stream. The default implementation of this method is exactly as seen when using the [[log.default_transport]] which logs to the console. The method should be overridden by subclasses to get a custom formatting.
When the last element of records is a dict, it’s treated as structured
fields rather than a plain message argument and rendered as trailing
key=value pairs: log.info('processing', { user_id: 5 }) renders as
... processing user_id=5. Fields bound via a [[log.Logger]] (through
context) are merged in the same way.
IMPORTANT!
The result of this function will be passed into the [[log.Transport.write()]] function so transport implementations MUST ensure to expect the same type as is returned from this function in the [[log.Transport.write()]] function message parameter.
Transport implementations should be aware of the following available private fields in the transport class:
- [[log.LogLevel]]
self._level- stringself._log_name- boolself._show_name- boolself._show_time- boolself._show_level
Parameters
records(list[any])level(log.LogLevel)context(?dict) — Optional{name: ?string, fields: dict}, supplied by a [[log.Logger]] call; absent for the flat module-levellog.info()-style calls.
Returns any
Transport.write()
log.Transport.write(message, level)
Do whatever it takes to actually log the specified logging record. This
method is intended to be implemented by subclasses and so raises an
Error if called directly from Transport.
Parameters
message(any)level(log.LogLevel)
Raises NotImplementedError
Transport.flush()
log.Transport.flush()
Ensure all logging output has been flushed to the target stream. This default version does nothing and is intended to be implemented by subclasses.
Transport.close()
log.Transport.close()
Tidy up any resources used by the transport. This default version does no output but removes the handler from an internal list of handlers. Subclasses should ensure that this gets called from overridden close() methods.
stat
import stat
The module provides constants and functions for interpreting results of file.stat().
The stat API
Every public name in stat, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
stat.FILE_ATTRIBUTE_ARCHIVE | constant | |
stat.FILE_ATTRIBUTE_COMPRESSED | constant | |
stat.FILE_ATTRIBUTE_DEVICE | constant | |
stat.FILE_ATTRIBUTE_DIRECTORY | constant | |
stat.FILE_ATTRIBUTE_ENCRYPTED | constant | |
stat.FILE_ATTRIBUTE_HIDDEN | constant | |
stat.FILE_ATTRIBUTE_INTEGRITY_STREAM | constant | |
stat.FILE_ATTRIBUTE_NORMAL | constant | |
stat.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED | constant | |
stat.FILE_ATTRIBUTE_NO_SCRUB_DATA | constant | |
stat.FILE_ATTRIBUTE_OFFLINE | constant | |
stat.FILE_ATTRIBUTE_READONLY | constant | |
stat.FILE_ATTRIBUTE_REPARSE_POINT | constant | |
stat.FILE_ATTRIBUTE_SPARSE_FILE | constant | |
stat.FILE_ATTRIBUTE_SYSTEM | constant | |
stat.FILE_ATTRIBUTE_TEMPORARY | constant | |
stat.FILE_ATTRIBUTE_VIRTUAL | constant | |
stat.SF_APPEND | constant | |
stat.SF_ARCHIVED | constant | |
stat.SF_IMMUTABLE | constant | |
stat.SF_NOUNLINK | constant | |
stat.SF_SNAPSHOT | constant | |
stat.ST_ATIME | constant | |
stat.ST_CTIME | constant | |
stat.ST_DEV | constant | |
stat.ST_GID | constant | |
stat.ST_INO | constant | |
stat.ST_MODE | constant | |
stat.ST_MTIME | constant | |
stat.ST_NLINK | constant | |
stat.ST_SIZE | constant | |
stat.ST_UID | constant | |
stat.S_ENFMT | constant | |
stat.S_IEXEC | constant | |
stat.S_IFBLK | constant | |
stat.S_IFCHR | constant | |
stat.S_IFDIR | constant | |
stat.S_IFDOOR | constant | |
stat.S_IFIFO | constant | |
stat.S_IFLNK | constant | |
stat.S_IFMT | function | Return the portion of the file’s mode that describes the file type. |
stat.S_IFPORT | constant | |
stat.S_IFREG | constant | |
stat.S_IFSOCK | constant | |
stat.S_IFWHT | constant | |
stat.S_IMODE | function | Return the portion of the file’s mode that can be set by file.chmod(). |
stat.S_IREAD | constant | |
stat.S_IRGRP | constant | |
stat.S_IROTH | constant | |
stat.S_IRUSR | constant | |
stat.S_IRWXG | constant | |
stat.S_IRWXO | constant | |
stat.S_IRWXU | constant | |
stat.S_ISBLK | function | Return true if mode is from a block special device file. |
stat.S_ISCHR | function | Return true if mode is from a character special device file. |
stat.S_ISDIR | function | Return true if mode is from a directory. |
stat.S_ISDOOR | function | Return true if mode is from a door. |
stat.S_ISFIFO | function | Return true if mode is from a FIFO (named pipe). |
stat.S_ISGID | constant | |
stat.S_ISLNK | function | Return true if mode is from a symbolic link. |
stat.S_ISPORT | function | Return true if mode is from an event port. |
stat.S_ISREG | function | Return true if mode is from a regular file. |
stat.S_ISSOCK | function | Return true if mode is from a socket. |
stat.S_ISUID | constant | |
stat.S_ISVTX | constant | |
stat.S_ISWHT | function | Return true if mode is from a whiteout. |
stat.S_IWGRP | constant | |
stat.S_IWOTH | constant | |
stat.S_IWRITE | constant | |
stat.S_IWUSR | constant | |
stat.S_IXGRP | constant | |
stat.S_IXOTH | constant | |
stat.S_IXUSR | constant | |
stat.UF_APPEND | constant | |
stat.UF_COMPRESSED | constant | |
stat.UF_HIDDEN | constant | |
stat.UF_IMMUTABLE | constant | |
stat.UF_NODUMP | constant | |
stat.UF_NOUNLINK | constant | |
stat.UF_OPAQUE | constant | |
stat.file_mode | function | Convert a file’s mode to a string of the form ‘-rwxrwxrwx’. |
Constants
ST_MODE
stat.ST_MODE = 0
ST_INO
stat.ST_INO = 1
ST_DEV
stat.ST_DEV = 2
ST_NLINK
stat.ST_NLINK = 3
ST_UID
stat.ST_UID = 4
ST_GID
stat.ST_GID = 5
ST_SIZE
stat.ST_SIZE = 6
ST_ATIME
stat.ST_ATIME = 7
ST_MTIME
stat.ST_MTIME = 8
ST_CTIME
stat.ST_CTIME = 9
S_IFDIR
stat.S_IFDIR = 16384
S_IFCHR
stat.S_IFCHR = 8192
S_IFBLK
stat.S_IFBLK = 24576
S_IFREG
stat.S_IFREG = 32768
S_IFIFO
stat.S_IFIFO = 4096
S_IFLNK
stat.S_IFLNK = 40960
S_IFSOCK
stat.S_IFSOCK = 49152
S_IFDOOR
stat.S_IFDOOR = 0
S_IFPORT
stat.S_IFPORT = 0
S_IFWHT
stat.S_IFWHT = 0
S_ISUID
stat.S_ISUID = 2048
S_ISGID
stat.S_ISGID = 1024
S_ENFMT
stat.S_ENFMT
S_ISVTX
stat.S_ISVTX = 512
S_IREAD
stat.S_IREAD = 256
S_IWRITE
stat.S_IWRITE = 128
S_IEXEC
stat.S_IEXEC = 64
S_IRWXU
stat.S_IRWXU = 448
S_IRUSR
stat.S_IRUSR = 256
S_IWUSR
stat.S_IWUSR = 128
S_IXUSR
stat.S_IXUSR = 64
S_IRWXG
stat.S_IRWXG = 56
S_IRGRP
stat.S_IRGRP = 32
S_IWGRP
stat.S_IWGRP = 16
S_IXGRP
stat.S_IXGRP = 8
S_IRWXO
stat.S_IRWXO = 7
S_IROTH
stat.S_IROTH = 4
S_IWOTH
stat.S_IWOTH = 2
S_IXOTH
stat.S_IXOTH = 1
UF_NODUMP
stat.UF_NODUMP = 1
UF_IMMUTABLE
stat.UF_IMMUTABLE = 2
UF_APPEND
stat.UF_APPEND = 4
UF_OPAQUE
stat.UF_OPAQUE = 8
UF_NOUNLINK
stat.UF_NOUNLINK = 16
UF_COMPRESSED
stat.UF_COMPRESSED = 32
UF_HIDDEN
stat.UF_HIDDEN = 32768
SF_ARCHIVED
stat.SF_ARCHIVED = 65536
SF_IMMUTABLE
stat.SF_IMMUTABLE = 131072
SF_APPEND
stat.SF_APPEND = 262144
SF_NOUNLINK
stat.SF_NOUNLINK = 1048576
SF_SNAPSHOT
stat.SF_SNAPSHOT = 2097152
FILE_ATTRIBUTE_ARCHIVE
stat.FILE_ATTRIBUTE_ARCHIVE = 32
FILE_ATTRIBUTE_COMPRESSED
stat.FILE_ATTRIBUTE_COMPRESSED = 2048
FILE_ATTRIBUTE_DEVICE
stat.FILE_ATTRIBUTE_DEVICE = 64
FILE_ATTRIBUTE_DIRECTORY
stat.FILE_ATTRIBUTE_DIRECTORY = 16
FILE_ATTRIBUTE_ENCRYPTED
stat.FILE_ATTRIBUTE_ENCRYPTED = 16384
FILE_ATTRIBUTE_HIDDEN
stat.FILE_ATTRIBUTE_HIDDEN = 2
FILE_ATTRIBUTE_INTEGRITY_STREAM
stat.FILE_ATTRIBUTE_INTEGRITY_STREAM = 32768
FILE_ATTRIBUTE_NORMAL
stat.FILE_ATTRIBUTE_NORMAL = 128
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED
stat.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 8192
FILE_ATTRIBUTE_NO_SCRUB_DATA
stat.FILE_ATTRIBUTE_NO_SCRUB_DATA = 131072
FILE_ATTRIBUTE_OFFLINE
stat.FILE_ATTRIBUTE_OFFLINE = 4096
FILE_ATTRIBUTE_READONLY
stat.FILE_ATTRIBUTE_READONLY = 1
FILE_ATTRIBUTE_REPARSE_POINT
stat.FILE_ATTRIBUTE_REPARSE_POINT = 1024
FILE_ATTRIBUTE_SPARSE_FILE
stat.FILE_ATTRIBUTE_SPARSE_FILE = 512
FILE_ATTRIBUTE_SYSTEM
stat.FILE_ATTRIBUTE_SYSTEM = 4
FILE_ATTRIBUTE_TEMPORARY
stat.FILE_ATTRIBUTE_TEMPORARY = 256
FILE_ATTRIBUTE_VIRTUAL
stat.FILE_ATTRIBUTE_VIRTUAL = 65536
Functions
S_IMODE()
stat.S_IMODE(mode: number) -> number
Return the portion of the file’s mode that can be set by file.chmod().
Parameters
mode(number)
Returns number
S_IFMT()
stat.S_IFMT(mode: number) -> number
Return the portion of the file’s mode that describes the file type.
Parameters
mode(number)
Returns number
S_ISDIR()
stat.S_ISDIR(mode: number) -> number
Return true if mode is from a directory.
Parameters
mode(number)
Returns number
S_ISCHR()
stat.S_ISCHR(mode: number) -> number
Return true if mode is from a character special device file.
Parameters
mode(number)
Returns number
S_ISBLK()
stat.S_ISBLK(mode: number) -> number
Return true if mode is from a block special device file.
Parameters
mode(number)
Returns number
S_ISREG()
stat.S_ISREG(mode: number) -> number
Return true if mode is from a regular file.
Parameters
mode(number)
Returns number
S_ISFIFO()
stat.S_ISFIFO(mode: number) -> number
Return true if mode is from a FIFO (named pipe).
Parameters
mode(number)
Returns number
S_ISLNK()
stat.S_ISLNK(mode: number) -> number
Return true if mode is from a symbolic link.
Parameters
mode(number)
Returns number
S_ISSOCK()
stat.S_ISSOCK(mode: number) -> number
Return true if mode is from a socket.
Parameters
mode(number)
Returns number
S_ISDOOR()
stat.S_ISDOOR(mode: number) -> number
Return true if mode is from a door.
Parameters
mode(number)
Returns number
S_ISPORT()
stat.S_ISPORT(mode: number) -> number
Return true if mode is from an event port.
Parameters
mode(number)
Returns number
S_ISWHT()
stat.S_ISWHT(mode: number) -> number
Return true if mode is from a whiteout.
Parameters
mode(number)
Returns number
file_mode()
stat.file_mode(mode: number) -> string
Convert a file’s mode to a string of the form ‘-rwxrwxrwx’.
Parameters
mode(number)
Returns string
2022, Richard Ore and The Zuri Contributors
test
import test
A testing framework: suites, assertions, mocks, snapshots, lifecycle hooks, and reports worth reading.
import test { * }
describe('Calculator', @{
it('adds', @{
expect(2 + 2).to_be(4)
})
it('divides', @{
expect(10 / 4).to_be_close_to(2.5)
})
})
Run it like any other script:
$ zuri run tests/calculator.zu
Calculator
✓ adds
✓ divides
PASS
2 passed • 2 total
suites 1 time 1ms
Importing
import test { * } is the form to use in a test file. It brings
describe, it, expect, mock, the hooks and run into scope,
which is what makes a test read as a test rather than as a series of
calls into a module.
import test is there for anything that is not a test file, and
qualifies the same names: test.conduct('tests'), test.run(). The two
can be combined when a test file wants both.
Declaring
describe(name, body) groups tests and may be nested as deeply as the
subject calls for. it(name, body) declares one. Neither runs anything:
they build a tree, and the tree is run once the file has finished
declaring everything in it. That is what makes focusing, filtering and
counting possible at all.
The run happens on its own, through os.at_exit(). Call run()
explicitly when you want to pass options or read the Summary back;
doing so takes over, and nothing runs a second time.
Focusing and skipping
describe_only('the one I am working on', @{ ... })
describe_skip('not now', @{ ... })
it_only('just this', @{ ... })
it_skip('known broken', @{ ... })
it_todo('handle an empty payload')
As soon as anything is marked only, everything not marked only and
not inside something marked only is skipped. skip always wins over
only.
it('name') with no body is a todo as well: it is reported as one and
counts towards nothing.
Table-driven tests
it_each([
[1, 1, 2],
[2, 3, 5],
[10, -4, 6],
], 'adds $0 and $1 to make $2', @(left, right, total) {
expect(left + right).to_be(total)
})
Each row becomes one test. $0, $1 and so on in the name are replaced
with that row’s values, and $# with the row number. describe_each()
does the same for whole suites.
Per-test options
it('reaches the network', @{ ... }, { retries: 2, tags: ['slow'] })
it('reproduces issue 412', @{ ... }, { failing: true })
it('stays fast', @{ ... }, { timeout: 50 })
| Option | What it does |
|---|---|
retries | run the body again on failure, up to this many times; passing after a retry is reported as flaky |
failing | the test passes when the body fails, and fails when it passes |
timeout | fail the test if it took longer than this many milliseconds |
tags | a list of labels to filter on |
Hooks
describe('Database', @{
before_all(@{ db.connect() })
after_all(@{ db.close() })
before_each(@{ db.begin() })
after_each(@{ db.rollback() })
it('inserts a row', @{ ... })
})
before_all runs once, immediately before the first test in its suite
that is actually going to run, and not at all for a suite everything was
filtered out of. after_all runs after the last one, and runs even when
before_all failed, so it must cope with a setup that got only part of
the way. before_each runs outermost first and after_each innermost
first, and after_each runs even when the test failed.
Asserting
expect(value) gives an object carrying every matcher, each of which
negates through .not and returns itself so several read as one:
expect(port).to_be_int().to_be_between(1024, 65535)
expect(items).not.to_be_empty()
The full list is in test.expect. In short:
- equality:
to_be,to_equal,to_be_same_as,to_be_close_to,to_be_within,to_match_object - truthiness:
to_be_true,to_be_false,to_be_truthy,to_be_falsy,to_be_nil,to_be_defined - types:
to_be_a,to_be_string,to_be_number,to_be_int,to_be_float,to_be_bigint,to_be_bool,to_be_list,to_be_dict,to_be_bytes,to_be_function,to_be_callable,to_be_iterable,to_be_class,to_be_instance,to_be_instance_of - numbers:
to_be_greater_than,to_be_greater_than_or_equal,to_be_less_than,to_be_less_than_or_equal,to_be_between,to_be_positive,to_be_negative,to_be_zero,to_be_divisible_by,to_be_even,to_be_odd,to_be_nan,to_be_finite,to_be_infinite - text:
to_contain,to_contain_ignoring_case,to_equal_ignoring_case,to_start_with,to_end_with,to_match,to_be_blank - collections:
to_have_length,to_be_empty,to_contain_equal,to_contain_all,to_contain_any,to_contain_none,to_contain_exactly,to_have_key,to_have_keys,to_have_value,to_have_property,to_be_sorted,to_have_unique_items - errors:
to_raise,to_raise_instance_of,to_raise_with_message,to_not_raise - mocks:
to_have_been_called,to_have_been_called_times,to_have_been_called_with,to_have_been_last_called_with,to_have_been_nth_called_with,to_have_returned,to_have_returned_with,to_have_raised - output:
to_print,to_print_exactly,to_print_nothing - snapshots:
to_match_snapshot - anything else:
to_satisfy
fail(message) fails outright, for the branch that should never be
reached. assertions(n) and has_assertions() fail a test that did not
assert what it said it would, which is how a test proves that the
callback it put its assertions in actually ran.
Mocking
var send = mock()
notify(send.fn)
expect(send).to_have_been_called_times(1)
expect(send).to_have_been_called_with('hello')
spy_on(object, 'key') replaces a function in place and restores it
after the test, whether the test passed or not. See
test.mock for what can and cannot be spied on.
Snapshots
expect(render(invoice)).to_match_snapshot()
The first run records the value beside the test file and passes; later
runs compare against it. See test.snapshot for the
file format, how to update them, and why a brand new snapshot fails on
CI.
Running
run() takes an options dictionary:
| Option | Default | What it does |
|---|---|---|
reporter | 'spec' | spec, dot, tap, junit, json, ndjson, silent, or a Reporter of your own |
filter | nil | run only tests whose full name contains this, or matches it when it is a regular expression |
tags | nil | run only tests carrying one of these tags |
exclude_tags | nil | skip tests carrying one of these tags |
bail | 0 | stop after this many failures; 0 never stops |
shuffle | false | run in a random order, to catch tests that depend on each other |
seed | nil | the seed to shuffle with; one is chosen and reported when not given |
slow | 300 | milliseconds past which a test’s time is highlighted |
timeout | 0 | a budget applied to every test; 0 means none |
retries | 0 | retries applied to every test |
capture | true | collect each test’s output and show it only when it fails |
verbose | false | show a passing test’s output as well |
update_snapshots | ZURI_UPDATE_SNAPSHOTS | rewrite snapshots instead of checking them |
ci | CI | treat a brand new snapshot as a failure |
exit | false | exit the process with the run’s status when it finishes |
filter and reporter can also come from ZURI_TEST_FILTER and
ZURI_TEST_REPORTER, which is what lets one command be pointed at one
test without editing the file.
run() returns a Summary; summary.exit_code() is 0 when
everything passed. A file that never calls run() gets the same status:
the automatic run sets it through os.set_exit_code() when anything
failed, so CI needs nothing added.
More than one file
conduct(directory) finds every test file under a directory, runs each
in a process of its own, and reports them together. A file that hangs or
crashes takes nothing else down with it. See
test.conduct.
zuri test is that call behind a command line, and is all a project
needs to run its tests directory:
$ zuri test
$ zuri test pricing # just tests/pricing.zu
Call conduct() yourself when the run wants to be under the project’s
own control:
# tests/index.zu
import os
import test
test.conduct(os.dir_name(__file__))
The test API
Every public name in test, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
test.AssertionError | class | A matcher that did not hold. |
test.Case | class | One declared test, and what became of it. |
test.Expect | class | The subject of an assertion, and every matcher that can be applied to it. |
test.Failure | class | One thing that went wrong. |
test.Mock | class | A recording stand-in for a function. |
test.Reporter | class | The interface the runner talks to, with every method doing nothing. |
test.Suite | class | A describe() block: its tests, its nested suites, and its hooks. |
test.Summary | class | What a whole run came to. |
test.TestSetupError | class | The framework was asked to do something that does not make sense: a matcher given the wrong kind of argument,… |
test.after_all | function | Runs once after the last test in the enclosing suite, and only if before_all ran. |
test.after_each | function | Runs after every test in the enclosing suite and everything nested inside it, innermost suite first, whether… |
test.assertions | function | Declares that this test makes exactly count assertions, and fails it if the number turns out to be… |
test.before_all | function | Runs once before the first test in the enclosing suite that is going to run. |
test.before_each | function | Runs before every test in the enclosing suite and everything nested inside it, outermost suite first. |
test.capture_output | function | Runs body and returns everything it printed to standard output. |
test.conduct | function | Discovers test files under directory and runs each in a process of its own. |
test.conduct.CONDUCTED | constant | |
test.conduct.FileResult | class | What became of one file. |
test.conduct.conduct | function | Runs every test file under directory, each in its own process, and reports them together. |
test.conduct.discover | function | Every test file under directory, in the order they will run. |
test.conduct.is_conducted | function | Whether this process was started by the conductor. |
test.context.Frame | class | The test the runner currently has open, as far as anything outside the runner needs to know about it. |
test.context.active | function | Whether a test is running right now. |
test.context.assertion_count | function | How many matchers have run in the current test. |
test.context.begin | function | Opens a frame for a test about to run. |
test.context.check | function | Checks a finished test’s assertion promises against what it actually did. |
test.context.current | function | The frame for the test currently running, or nil outside one. |
test.context.end | function | Closes the current frame and returns it, so the runner can read the final assertion count off it. |
test.context.expect_assertions | function | Declares that the current test makes exactly count assertions. |
test.context.expect_some_assertions | function | Declares that the current test makes at least one assertion. |
test.context.record_assertion | function | Counts one matcher against the current test. |
test.context.snapshot_key | function | Claims the next snapshot key for the current test. |
test.declared | function | The tests declared so far, as a tree, without running any of them. |
test.describe | function | Groups tests, and may be nested. |
test.describe_each | function | Declares one suite per row of a table. |
test.describe_only | function | Runs only this suite, and anything else marked only. |
test.describe_skip | function | Skips this suite. |
test.diff.MAX_DIFF_DEPTH | constant | |
test.diff.MAX_DIFF_LINES | constant | |
test.diff.equal | function | Whether left and right are structurally equal. |
test.diff.render | function | The lines to print underneath a failure message, showing what the two values disagree about. |
test.diff.subset | function | Whether every key in subset is present in value with a structurally equal value, ignoring any key value… |
test.expect | function | Starts an assertion about value. |
test.fail | function | Fails the current test outright. |
test.format.DEFAULT_MAX_DEPTH | constant | |
test.format.DEFAULT_MAX_ITEMS | constant | |
test.format.DEFAULT_MAX_STRING | constant | |
test.format.block | function | value rendered over as many lines as it takes, with nothing abbreviated away. |
test.format.class_name | function | The name of the class value was built from, or nil when it is not an instance. |
test.format.duration | function | A duration in milliseconds, rendered the way a reader wants to see it: whole milliseconds below a second, and… |
test.format.inline | function | value rendered as a single line, abbreviated to stay readable inside a sentence. |
test.format.inline_plain | function | inline() with colour suppressed, whatever the terminal supports. |
test.format.plural | function | count followed by singular or plural, whichever the count calls for. |
test.format.properties_of | function | The names of an instance’s own properties, in declaration order. |
test.format.property_of | function | Reads one of an instance’s own properties by name. |
test.format.serialize | function | block() with colour suppressed and dictionary keys sorted, which together make the output stable enough to… |
test.format.type_label | function | How to name value’s type in a sentence a person reads. |
test.format.type_name | function | The name of value’s type: 'nil', 'bool', 'int', 'float', 'bigint', 'string', 'bytes',… |
test.format.with_article | function | word with the article that belongs in front of it. |
test.has_assertions | function | Declares that this test makes at least one assertion, and fails it if none did. |
test.it | function | Declares one test. |
test.it_each | function | Declares one test per row of a table. |
test.it_only | function | Runs only this test, and anything else marked only. |
test.it_skip | function | Skips this test, while still listing it in the report. |
test.it_todo | function | Notes a test that has not been written yet. |
test.mock | function | A new recording function. |
test.mock.Call | class | One recorded call. |
test.mock.mock_of | function | The Mock behind value, which may be a Mock already or the plain function one handed out. |
test.mock.reset_all | function | Forgets the calls recorded by every live mock, leaving the mocks themselves and their behaviours in place. |
test.mock.restore_all | function | Undoes every spy and forgets every mock built so far. |
test.reporter.Dot | class | One character per test, wrapped to the terminal, then the same failure detail and summary the spec reporter… |
test.reporter.Json | class | One JSON document at the end, holding the summary and every test. |
test.reporter.Junit | class | JUnit XML, which is the format nearly every CI system knows how to turn into a test report page. |
test.reporter.Ndjson | class | One JSON object per line, emitted as each thing happens. |
test.reporter.PREFIX | constant | What marks a line of ndjson output as protocol rather than as something the test file happened to print. |
test.reporter.Silent | class | No output at all, for a caller reading the returned Summary instead. |
test.reporter.Spec | class | The default: the suite tree, one line per test, and every failure written out in full at the end. |
test.reporter.Tap | class | TAP version 14: one ok/not ok line per test, with failure detail in a YAML block underneath. |
test.reporter.create | function | Builds a reporter by name. |
test.reset | function | Forgets every declaration and every loaded snapshot file. |
test.result.FAILED | constant | It ran and something did not hold. |
test.result.FLAKY | constant | It failed, was retried, and then passed. |
test.result.PASSED | constant | It ran and every assertion held. |
test.result.PENDING | constant | A test was declared and not yet run. |
test.result.SKIPPED | constant | It was not run: skip, a filter, or another test’s only. |
test.result.TODO | constant | It was declared with no body, as a note to write it later. |
test.run | function | Runs everything declared so far, and reports it. |
test.runner.after_all | function | |
test.runner.after_each | function | |
test.runner.before_all | function | |
test.runner.before_each | function | |
test.runner.describe | function | Opens a suite, runs body to collect what is inside it, and closes it again. |
test.runner.it | function | Declares one test. |
test.runner.reset | function | Throws away every declaration and every snapshot store, so a second run in the same process starts from… |
test.runner.root | function | The tree as it stands, for a caller that wants to look at what was declared without running it. |
test.runner.run | function | Runs everything declared so far and reports it. |
test.snapshot.BODY_INDENT | constant | |
test.snapshot.HEADER | constant | |
test.snapshot.Store | class | Every snapshot recorded for one test file, and the file they live in. |
test.snapshot.check | function | Compares one value against what the snapshot file holds for key, writing it down when there is nothing… |
test.snapshot.flush | function | Writes every changed store back to disk, and in update mode drops entries the run never asked about. |
test.snapshot.path_for | function | The .snap file that belongs to a given test file. |
test.snapshot.recorded_lines | function | The lines of the snapshot at key, as a list, for a reporter that wants to show what was recorded. |
test.snapshot.reset | function | Forgets every loaded store and zeroes the counters, so a second run in the same process starts clean. |
test.snapshot.set_strict | function | Turns the strict, new-snapshots-fail behaviour on or off. |
test.snapshot.set_updating | function | Turns snapshot rewriting on or off. |
test.snapshot.store_for | function | The store for a test file, read from disk the first time it is asked for and kept afterwards. |
test.snapshot.strict | function | Whether a brand new snapshot counts as a failure, which is what it should be anywhere nobody is going to look… |
test.snapshot.summary | function | What happened to snapshots over the whole run. |
test.snapshot.updating | function | Whether snapshots are being rewritten rather than checked. |
test.source.Frame | class | One frame of a stack trace, pulled apart. |
test.source.clear_cache | function | Forgets every file read for a code frame. |
test.source.code_frame | function | The source around a failure, with the offending line marked. |
test.source.origin | function | The innermost frame of stacktrace outside the test module: where the failing line actually is. |
test.source.parse_frame | function | Splits one stack trace line into a Frame. |
test.source.short_path | function | A path written relative to the working directory when it is under it, and left alone when it is not. |
test.source.user_frames | function | Every frame of stacktrace that belongs to the code under test, innermost first. |
test.spy_on | function | Replaces target[key] with a recording stand-in that calls through to the original, and returns the Mock… |
test.style.badge | function | A filled, inverted label, the way a status banner reads in a CI log. |
test.style.blue | function | Todo entries and other informational notes. |
test.style.bold | function | Emphasis. |
test.style.cyan | function | Structure: suite names, headings. |
test.style.dim | function | De-emphasis, for detail that should recede: timings, counts, the suite path above a failure. |
test.style.enabled | function | Whether colour is currently being emitted. |
test.style.green | function | Success. |
test.style.grey | function | Punctuation and separators. |
test.style.indent | function | Two spaces per level, the indent every nested suite and test line in the report is built from. |
test.style.inverse | function | Reserved for text that has to be found instantly on a busy screen. |
test.style.magenta | function | Values in a rendered diff or failure message. |
test.style.pad_left | function | Pads text on the left to width visible columns, leaving it alone when it is already that wide or wider. |
test.style.pad_right | function | Pads text on the right to width visible columns, leaving it alone when it is already that wide or wider. |
test.style.red | function | Failure. |
test.style.rule | function | count copies of character. |
test.style.set_enabled | function | Forces colour on or off, overriding what the environment said. |
test.style.set_unicode | function | Forces the Unicode symbol set on or off. |
test.style.strip | function | text with every ANSI escape sequence removed. |
test.style.symbols | function | The symbol set in use, keyed by role. |
test.style.truncate | function | Shortens text to at most width visible columns, marking the cut with an ellipsis. |
test.style.unicode | function | Whether the Unicode symbol set is in use, as opposed to the ASCII fallbacks. |
test.style.visible_length | function | How many columns text occupies once its escape sequences are discounted. |
test.style.white | function | Plain foreground, used to lift a key out of dimmed surroundings. |
test.style.width | function | The width to lay the report out to. |
test.style.yellow | function | Skipped and other deliberate non-results. |
test.use_color | function | Forces colour in the report on or off, overriding what the environment said. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
test.conduct | import test.conduct | Running a directory of test files, each in a process of its own. |
test.context | import test.context | What is true right now, while one particular test is running. |
test.diff | import test.diff | Structural equality, and the rendering of what two unequal values disagree about. |
test.error | test.error.* | The errors the test module raises, kept in a module of their own so that the assertion side and the runner… |
test.expect | test.expect.* | expect(value) and everything that can be said about a value once you have one. |
test.format | import test.format | Turning any Zuri value into text a person can read in a failure message. |
test.mock | test.mock.* | Test doubles: functions that record how they were called, and stand-ins that replace a real one for the… |
test.reporter | test.reporter.* | How a run is shown. |
test.result | test.result.* | The shapes a test run is made of: the tree the declarations build, and what running it produced. |
test.runner | import test.runner | Collecting the tests a file declares, deciding which of them to run, and running them. |
test.snapshot | import test.snapshot | Snapshot testing: recording what a value looked like the first time and failing when it stops looking like… |
test.source | import test.source | Working out where a failure came from, and showing the code that was there. |
test.style | import test.style | Terminal presentation for the test module: colour, symbols, width, and the padding helpers that keep a… |
Functions
describe()
test.describe(name: string, body: function) -> Suite
Groups tests, and may be nested.
describe('Stack', @{
describe('push()', @{
it('grows the stack', @{ ... })
})
})
Parameters
name(string)body(function)
Returns Suite
describe_only()
test.describe_only(name: string, body: function) -> Suite
Runs only this suite, and anything else marked only.
Parameters
name(string)body(function)
Returns Suite
describe_skip()
test.describe_skip(name: string, body: function) -> Suite
Skips this suite. Its tests are still declared, still counted, and still listed; they simply do not run.
Parameters
name(string)body(function)
Returns Suite
it()
test.it(name: string, body: ?function, options: ?dict) -> Case
Declares one test.
it('returns the total', @{
expect(total(cart)).to_be(42)
})
Parameters
name(string)body(?function) — left out, this is a todo.options(?dict) —retries,failing,timeout,tags. See the module documentation for what each does.
Returns Case
it_only()
test.it_only(name: string, body: function, options: ?dict) -> Case
Runs only this test, and anything else marked only.
Parameters
name(string)body(function)options(?dict)
Returns Case
it_skip()
test.it_skip(name: string, body: ?function, options: ?dict) -> Case
Skips this test, while still listing it in the report.
Parameters
name(string)body(?function)options(?dict)
Returns Case
it_todo()
test.it_todo(name: string) -> Case
Notes a test that has not been written yet.
It is listed in the report and counts towards nothing, which is the point: a todo is a reminder that survives being committed.
Parameters
name(string)
Returns Case
it_each()
test.it_each(table: list, name: string, body: function, options: ?dict)
Declares one test per row of a table.
it_each([
['', 0],
['a', 1],
['hello', 5],
], 'says $0 is $1 characters long', @(text, length) {
expect(text.length()).to_be(length)
})
Each row is spread across body’s parameters, so a three-column table
calls a three-parameter function. A row that is not a list counts as a
single-column row.
Parameters
table(list) — the rows.name(string) —$0,$1, … are replaced with that row’s values, and$#with the row number, counting from one.body(function)options(?dict) — applied to every test the table declares.
Returns — list: the declared tests.
describe_each()
test.describe_each(table: list, name: string, body: function)
Declares one suite per row of a table.
The row is spread across body’s parameters exactly as it_each()
does, so the whole suite is written once against whatever the row holds.
describe_each([
['json', json_codec],
['yaml', yaml_codec],
], '$0 codec', @(name, codec) {
it('round-trips a dictionary', @{
expect(codec.decode(codec.encode({ a: 1 }))).to_equal({ a: 1 })
})
})
Parameters
table(list)name(string)body(function)
Returns — list: the declared suites.
before_all()
test.before_all(body: function)
Runs once before the first test in the enclosing suite that is going to run.
Parameters
body(function)
after_all()
test.after_all(body: function)
Runs once after the last test in the enclosing suite, and only if
before_all ran. It runs when before_all raised as well, since a
setup that failed part way may already hold what needs releasing, so it
must cope with whatever the setup got as far as.
Parameters
body(function)
before_each()
test.before_each(body: function)
Runs before every test in the enclosing suite and everything nested inside it, outermost suite first.
Parameters
body(function)
after_each()
test.after_each(body: function)
Runs after every test in the enclosing suite and everything nested inside it, innermost suite first, whether the test passed or not.
Parameters
body(function)
run()
test.run(options: ?dict) -> Summary
Runs everything declared so far, and reports it.
Calling this is optional. A file that declares tests and never says anything else runs them as it ends, and fails the process when they fail. Call it when you want to pass options or read the result:
run({ reporter: 'dot', bail: 1 })
var summary = run({ filter: 'parser' })
echo summary.passed
Parameters
options(?dict) — see the module documentation for every option and its default.
Returns Summary
Note: calling it takes over from the automatic run, so nothing is reported twice. Calling it twice does run everything twice.
conduct()
test.conduct(directory: ?string, options: ?dict)
Discovers test files under directory and runs each in a process of its
own.
Parameters
directory(?string) —'tests'by default.options(?dict) — see {test.conduct}.
Returns — dict: the combined result.
declared()
test.declared() -> Suite
The tests declared so far, as a tree, without running any of them.
Returns Suite
reset()
test.reset()
Forgets every declaration and every loaded snapshot file.
For a process that runs more than one suite, and for the tests of this framework itself.
use_color()
test.use_color(on: bool)
Forces colour in the report on or off, overriding what the environment said.
Parameters
on(bool)
2021, Richard Ore and Zuri contributors
test.conduct
import test.conduct
testdoes not re-export this module, so it is reached only by importing it directly.
Running a directory of test files, each in a process of its own.
import test
test.conduct('tests')
zuri test is this call behind a command line, with every option below
as a flag, so a project gets a test runner without writing one. Call
conduct() directly when the run wants to be under the project’s own
control.
One process per file
A test file is an ordinary Zuri script, and each one is run as one:
- A file that loops forever is killed, and the rest still run.
- A file that segfaults, or calls
os.exit()halfway through, is reported as a crashed file rather than taking the run with it. - Global state, a module loaded with a side effect, a signal handler, a changed working directory: none of it leaks from one file into the next.
timeoutis enforced bykill, so it holds for a file that never returns.
The index is not a test file
index.zu is left out of discovery. A directory handed to zuri run
runs its index.zu, so a suite that wants to start itself puts one
there:
# tests/index.zu
import os
import test
test.conduct(os.dir_name(__file__))
$ zuri run tests
The script that is currently running is left out as well, whatever it is called, so no arrangement of files makes a run start itself again.
What a test file has to do
Nothing special. It declares its tests. The conductor sets
ZURI_TEST_CONDUCTED in the child, and the run switches to a
machine-readable reporter when it sees it, so the same file is equally
runnable on its own.
A file that declares no tests at all, or exits before it can report, is called out rather than silently counting as zero tests.
Ordering
Files are reported in the order they were discovered, whatever order
they finish in, so a run is reproducible with jobs turned up.
Constants
CONDUCTED
test.conduct.CONDUCTED = 'ZURI_TEST_CONDUCTED'
Functions
discover()
test.conduct.discover(directory: string, options: ?dict)
Every test file under directory, in the order they will run.
index.zu is never one of them. A directory handed to zuri run runs
its index.zu, so that file is the thing that starts a suite rather
than a part of one, and conducting a directory from inside its own index
is the shape this is written for. Pass an ignore list of your own when
a file of that name really is a test.
The script that is currently running is left out too, whatever it is called, so no arrangement of files can make a run start itself again.
Parameters
directory(string)options(?dict) —match,ignoreandrecursive, asconduct()takes them.
Returns — list: paths, relative to directory.
conduct()
test.conduct.conduct(directory: ?string, options: ?dict)
Runs every test file under directory, each in its own process, and
reports them together.
import test
test.conduct('tests', { timeout: 30000, jobs: 4 })
The process ends with status 1 when anything failed, through
os.set_exit_code(), so nothing has to be added for CI. The returned
dictionary is there for a caller that wants to look at the run rather
than just be told about it.
match: filename patterns to run,['*.zu']by default.*and?are the only wildcards.ignore: patterns for files and directories to skip,['_*', '.*', 'index.zu']by default, which is what keeps helper files,__snapshots__and the index that starts the suite out of it.recursive: whether to look in subdirectories.true.jobs: how many files to run at once.1, because a test file that binds a port or writes a fixture was probably not written to share the machine. Reporting order is by discovery whatever this is set to.timeout: milliseconds to give one file before killing it.0, meaning no limit.bail: stop after this many failing files.0never stops.env: extra environment variables for every child.
Parameters
directory(?string) —'tests'by default.options(?dict)
Returns — dict: { files, totals, duration, exit_code, ok }, where
files is a list of FileResult.
Raises TestSetupError when directory does not exist.
Note:
index.zuis never one of the files it runs, and neither is the script that called it, so atests/index.zuconducting its own directory works.
is_conducted()
test.conduct.is_conducted() -> bool
Whether this process was started by the conductor.
run() uses it to switch to the machine-readable reporter. A test file
has no reason to care.
Returns bool
Classes
FileResult
class test.conduct.FileResult
What became of one file.
Fields
| Field | Type | Description |
|---|---|---|
path | The path, relative to the directory that was conducted. | |
code | The child’s exit status. | |
duration | How long the child ran, in milliseconds. | |
tests | The test_finished payloads the child reported. | |
summary | The child’s own summary, or nil when it never reported one. | |
output | Anything the child printed that was not part of the protocol. | |
problem | Why the file did not report properly, when it did not. |
Constructor
test.conduct.FileResult(path)
FileResult.ok()
test.conduct.FileResult.ok()
Whether the file ran cleanly and everything in it passed. @returns bool
test.context
import test.context
testdoes not re-export this module, so it is reached only by importing it directly.
What is true right now, while one particular test is running.
The assertion side and the runner side both need this, and neither
should have to import the other, so it lives on its own. Nothing in here
is interesting to a test author; the API a test uses is
test.assertions() and test.has_assertions(), both of which are thin
wrappers over what is below.
The state is a single module-level value because a Zuri program has one call stack and runs one test at a time. There is nothing to key it by.
Functions
begin()
test.context.begin(name: string, suite_path: list, file: string) -> Frame
Opens a frame for a test about to run. Called by the runner, once per attempt.
Parameters
name(string)suite_path(list)file(string)
Returns Frame
end()
test.context.end()
Closes the current frame and returns it, so the runner can read the final assertion count off it.
Returns — Frame: nil when no test was open.
current()
test.context.current() -> Frame
The frame for the test currently running, or nil outside one.
Returns Frame
active()
test.context.active() -> bool
Whether a test is running right now.
Matchers use this to tell a real assertion failure, which the runner will catch and report, from one raised in a suite body or at the top level of a file, where there is nothing to attribute it to.
Returns bool
record_assertion()
test.context.record_assertion()
Counts one matcher against the current test. Every matcher calls this, whether it passes or fails.
assertion_count()
test.context.assertion_count()
How many matchers have run in the current test.
Returns — int: 0 outside a test.
expect_assertions()
test.context.expect_assertions(count: int)
Declares that the current test makes exactly count assertions.
Parameters
count(int)
Raises TestSetupError outside a test, where there is nothing to
hold to the promise.
expect_some_assertions()
test.context.expect_some_assertions()
Declares that the current test makes at least one assertion.
Raises TestSetupError outside a test.
check()
test.context.check(frame)
Checks a finished test’s assertion promises against what it actually did.
Parameters
frame(Frame)
Returns — string: what went wrong, or nil when nothing did.
snapshot_key()
test.context.snapshot_key(name: ?string) -> string
Claims the next snapshot key for the current test.
A named snapshot keeps its name. An unnamed one is numbered in the order it was taken, so reordering the snapshots inside one test is a change the snapshot file will notice.
Parameters
name(?string)
Returns string
Raises TestSetupError outside a test.
Classes
Frame
class test.context.Frame
The test the runner currently has open, as far as anything outside the runner needs to know about it.
Fields
| Field | Type | Description |
|---|---|---|
name | The test’s own name. | |
suite_path | Enclosing suite names, outermost first. | |
file | The file the test was declared in. | |
assertions | Matchers that have run since the test started. | |
expected_assertions | How many assertions the test said it would make, or -1 when it did not say. | |
requires_assertions | Whether the test asked to be failed if it asserts nothing. | |
snapshot_count | Snapshot names already used by this test, so an unnamed snapshot can be numbered 1, 2, 3 in the order… |
Constructor
test.context.Frame(name, suite_path, file)
Frame.full_name()
test.context.Frame.full_name() -> string
The test’s full name, suite path included, as it appears in a report and in a snapshot file.
Returns string
test.diff
import test.diff
testdoes not re-export this module, so it is reached only by importing it directly.
Structural equality, and the rendering of what two unequal values disagree about.
equal() is what to_equal() is built on, and it is worth knowing
exactly how it differs from ==:
== | equal() | |
|---|---|---|
| list, dict, bytes | compares contents | compares contents |
| instance | compares identity | compares class, then every field |
NaN | never equal to anything | equal to NaN |
-0 and 0 | equal | equal |
| int and bigint | never equal | never equal |
| cyclic values | hangs | compares, treating a repeated pair as equal |
The one that surprises people is instances: Point(1, 2) == Point(1, 2)
is false, because two separately constructed objects are two objects.
equal() is the one to reach for when what you care about is the
contents.
render() turns a failed comparison into lines to print. It picks the
shape that shows the most: a line diff for multi-line strings, a
character marker for single-line ones, a keyed structural diff for
dictionaries and instances, a positional one for lists, and a plain
two-line expected/received block for anything else.
Constants
MAX_DIFF_DEPTH
test.diff.MAX_DIFF_DEPTH = 6
MAX_DIFF_LINES
test.diff.MAX_DIFF_LINES = 30
Functions
equal()
test.diff.equal(left, right) -> bool
Whether left and right are structurally equal.
Parameters
left(any)right(any)
Returns bool
subset()
test.diff.subset(expected: dict, value) -> bool
Whether every key in subset is present in value with a structurally
equal value, ignoring any key value has that subset does not
mention.
Nested dictionaries recurse, so { user: { id: 1 } } matches { user: { id: 1, name: 'Ada' } }.
Lists do not: a list in subset must equal the list in value
outright, since “some of these elements, in no particular place” is a
different question and has its own matcher.
Instances are matched by property, so a dictionary of expectations can be checked against a real object.
Parameters
subset(dict)value(any)
Returns bool
render()
test.diff.render(expected, received)
The lines to print underneath a failure message, showing what the two values disagree about.
Every line is already coloured and carries no indent of its own; the reporter decides how far in the whole block sits.
import test.diff
for line in diff.render([1, 2, 3], [1, 9, 3]) {
echo line
}
Parameters
expected(any)received(any)
Returns — list: the lines, or an empty list when the two are equal.
test.error
import test.error
testlifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledtest.error.*needsimport test.error.
The errors the test module raises, kept in a module of their own so that the assertion side and the runner side can both name them without importing each other.
Only AssertionError normally reaches a test author, and only by being
caught for them. Everything else in here marks a mistake in how the
framework itself was used, and is meant to stop the run.
Classes
AssertionError
class test.AssertionError < Error
A matcher that did not hold.
The runner catches these, so a test body never has to. What separates one from any other error raised inside a test is that the framework knows how to display it: it carries the two values that disagreed, and whatever extra lines (a diff, a hint) the matcher wanted shown underneath.
import test { AssertionError }
catch {
expect(1).to_be(2)
} as error {
echo error.matcher
echo error.expected
echo error.received
}
to_be
2
1
Fields
| Field | Type | Description |
|---|---|---|
matcher | The matcher that failed, e.g. 'to_be', 'to_contain'. | |
expected | What the matcher was told to expect. | |
received | What it was actually given. | |
has_values | Whether expected and received are worth showing. | |
details | Pre-rendered lines to print underneath the message: a structural diff, the calls a mock actually received, a… |
Constructor
test.AssertionError(message, info)
Parameters
message(string) — the one-line summary.info(?dict) — any ofmatcher,expected,received,has_values,details.
TestSetupError
class test.TestSetupError < Error
The framework was asked to do something that does not make sense: a matcher given the wrong kind of argument, a hook registered outside any suite, a reporter name that does not exist.
This is never a test failing. It stops the run, because carrying on would report results that do not mean what they say.
Constructor
test.TestSetupError(message)
test.expect
import test.expect
testlifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledtest.expect.*needsimport test.expect.
expect(value) and everything that can be said about a value once you
have one.
expect(total).to_be(42)
expect(user).to_match_object({ name: 'Ada' })
expect(items).not.to_be_empty()
expect(@{ parse('') }).to_raise_instance_of(ValueError)
Negation
Every matcher negates through .not, and there is exactly one
implementation behind both directions, so a matcher and its negation can
never disagree about what they mean.
Chaining
Matchers return the Expect they were called on, so several statements
about one value read as one:
expect(port).to_be_int().to_be_between(1024, 65535)
Labels
A second argument to expect() names the value, which is worth doing
when the number alone would not say which number it was:
expect(response.status, 'status').to_be(200)
Expected status (404) to be 200
Choosing between to_be and to_equal
to_be is Zuri’s ==. That already compares lists, dictionaries and
bytes by their contents, so it is the right matcher for most things. It
compares two instances by identity, though, so two separately
constructed objects are never to_be each other. to_equal is the
structural one: it walks instances field by field, treats NaN as equal
to itself, and survives a value that contains itself.
Functions
expect()
test.expect(value, label: ?string) -> Expect
Starts an assertion about value.
expect(total).to_be(42)
expect(total, 'order total').to_be(42)
Parameters
value(any)label(?string) — a name for the value, shown in failure messages ahead of the value itself.
Returns Expect
fail()
test.fail(message: ?string)
Fails the current test outright.
For the branch that should never be reached:
using response.status {
when 200 handle_ok()
when 404 handle_missing()
default fail('unexpected status ${response.status}')
}
Parameters
message(?string)
Raises AssertionError always.
assertions()
test.assertions(count: int)
Declares that this test makes exactly count assertions, and fails it
if the number turns out to be different.
This is how a test proves that a branch it expected to reach was actually reached, which matters most when the assertions are inside a callback that might never run:
it('reports every error', @{
assertions(2)
validate(bad_input, @(error) {
expect(error.field).to_be_string()
})
})
Parameters
count(int)
Raises TestSetupError outside a test.
has_assertions()
test.has_assertions()
Declares that this test makes at least one assertion, and fails it if none did.
Raises TestSetupError outside a test.
capture_output()
test.capture_output(body: function) -> string
Runs body and returns everything it printed to standard output.
The composable half of to_print(): use this when the assertion to make
about the output is not “it contains this”.
var printed = capture_output(@{ report(rows) })
expect(printed.lines()).to_have_length(4)
Parameters
body(function)
Returns string
Classes
Expect
class test.Expect
The subject of an assertion, and every matcher that can be applied to it.
Built by expect(); there is no reason to construct one directly.
Fields
| Field | Type | Description |
|---|---|---|
not | The same subject, with every matcher asserting the opposite. |
Constructor
test.Expect(value, label, negated, twin)
Parameters
value(any) — the value under test.label(?string) — a name for it, used in failure messages.negated(?bool)twin(?Expect) — the opposite-polarity object to pointnotat. Left out, one is built; this is what stops the pair from constructing each other forever.
Expect.to_be()
test.Expect.to_be(expected) -> Expect
Asserts ==.
Numbers, strings and booleans compare by value; lists, dictionaries and bytes compare by contents; instances and functions compare by identity.
expect(2 + 2).to_be(4)
expect([1, 2]).to_be([1, 2])
Parameters
expected(any)
Returns Expect
Expect.to_equal()
test.Expect.to_equal(expected) -> Expect
Asserts structural equality: instances are compared field by field
rather than by identity, NaN equals NaN, and a value that contains
itself is handled rather than hung on.
expect(Point(1, 2)).to_equal(Point(1, 2))
Parameters
expected(any)
Returns Expect
Expect.to_be_same_as()
test.Expect.to_be_same_as(expected) -> Expect
Asserts that this is the very same object, not merely one that looks the
same. Two lists with identical contents are to_be each other but not
to_be_same_as each other.
var original = [1, 2]
expect(passed_through(original)).to_be_same_as(original)
Parameters
expected(any)
Returns Expect
Expect.to_be_close_to()
test.Expect.to_be_close_to(expected: number, digits: ?int) -> Expect
Asserts two numbers agree to digits decimal places, which is the only
sane way to compare anything that has been through floating-point
arithmetic.
expect(0.1 + 0.2).to_be_close_to(0.3)
Parameters
expected(number)digits(?int) — decimal places,2by default, so the default tolerance is 0.005.
Returns Expect
Expect.to_be_within()
test.Expect.to_be_within(expected: number, delta: number) -> Expect
Asserts two numbers are no more than delta apart, when the tolerance
is better stated as an absolute amount than as a number of decimal
places.
expect(elapsed_ms).to_be_within(500, 50)
Parameters
expected(number)delta(number) — inclusive.
Returns Expect
Expect.to_match_object()
test.Expect.to_match_object(expected: dict) -> Expect
Asserts that every key in expected is present with a matching value,
ignoring anything else the subject carries.
Nested dictionaries recurse, and the subject may be a dictionary or an instance, so one expectation can be checked against either.
expect(create_user('Ada')).to_match_object({ name: 'Ada', active: true })
Parameters
expected(dict)
Returns Expect
Expect.to_be_true()
test.Expect.to_be_true()
Asserts the value is exactly true. @returns Expect
Expect.to_be_false()
test.Expect.to_be_false()
Asserts the value is exactly false. @returns Expect
Expect.to_be_truthy()
test.Expect.to_be_truthy() -> Expect
Asserts the value is truthy.
Returns Expect
Note: Zuri’s falsy values are
false,nil, zero,NaN, a zero bigint, the empty string and an empty bytes buffer. Every negative number is truthy, and so is an empty list or dictionary; useto_be_empty()for those.
Expect.to_be_falsy()
test.Expect.to_be_falsy() -> Expect
Asserts the value is falsy.
Returns Expect
Note: see
to_be_truthy()for the full list of falsy values.
Expect.to_be_nil()
test.Expect.to_be_nil()
Asserts the value is nil. @returns Expect
Expect.to_be_defined()
test.Expect.to_be_defined() -> Expect
Asserts the value is anything other than nil, which is the assertion
to reach for after a lookup that can come back empty.
Returns Expect
Expect.to_be_a()
test.Expect.to_be_a(type) -> Expect
Asserts the value’s type.
type may be a type name as a string, one of the names
format.type_name() produces, or a class, in which case this means the
same as to_be_instance_of().
expect(payload).to_be_a('dict')
expect(result).to_be_a(Point)
Parameters
type(string|class)
Returns Expect
Expect.to_be_string()
test.Expect.to_be_string()
Asserts the value is a string. @returns Expect
Expect.to_be_number()
test.Expect.to_be_number()
Asserts the value is a number, of either kind. @returns Expect
Expect.to_be_int()
test.Expect.to_be_int()
Asserts the value is a number with no fractional part. @returns Expect
Expect.to_be_float()
test.Expect.to_be_float()
Asserts the value is a number with a fractional part. @returns Expect
Expect.to_be_bigint()
test.Expect.to_be_bigint()
Asserts the value is an arbitrary-precision integer. @returns Expect
Expect.to_be_bool()
test.Expect.to_be_bool()
Asserts the value is a boolean. @returns Expect
Expect.to_be_list()
test.Expect.to_be_list()
Asserts the value is a list. @returns Expect
Expect.to_be_dict()
test.Expect.to_be_dict()
Asserts the value is a dictionary. @returns Expect
Expect.to_be_bytes()
test.Expect.to_be_bytes()
Asserts the value is a bytes buffer. @returns Expect
Expect.to_be_function()
test.Expect.to_be_function()
Asserts the value is a function. @returns Expect
Expect.to_be_callable()
test.Expect.to_be_callable() -> Expect
Asserts the value can be called, which a class and a bound method can be as well as a function.
Returns Expect
Expect.to_be_iterable()
test.Expect.to_be_iterable() -> Expect
Asserts the value can be iterated with for, which covers lists,
dictionaries, strings, ranges, bytes, and any class defining @key and
@value.
Returns Expect
Expect.to_be_class()
test.Expect.to_be_class()
Asserts the value is a class, not an instance of one. @returns Expect
Expect.to_be_instance()
test.Expect.to_be_instance()
Asserts the value is an instance of some class. @returns Expect
Expect.to_be_instance_of()
test.Expect.to_be_instance_of(klass: type) -> Expect
Asserts the value is an instance of klass or of anything that inherits
from it.
expect(error).to_be_instance_of(ValueError)
Parameters
type— klass
Returns Expect
Expect.to_be_greater_than()
test.Expect.to_be_greater_than(bound: number)
Parameters
bound(number) — @returns Expect
Expect.to_be_greater_than_or_equal()
test.Expect.to_be_greater_than_or_equal(bound: number)
Parameters
bound(number) — @returns Expect
Expect.to_be_less_than()
test.Expect.to_be_less_than(bound: number)
Parameters
bound(number) — @returns Expect
Expect.to_be_less_than_or_equal()
test.Expect.to_be_less_than_or_equal(bound: number)
Parameters
bound(number) — @returns Expect
Expect.to_be_between()
test.Expect.to_be_between(low: number, high: number) -> Expect
Asserts the value falls in [low, high], both ends included.
Parameters
low(number)high(number)
Returns Expect
Expect.to_be_positive()
test.Expect.to_be_positive()
Asserts the value is greater than zero. @returns Expect
Expect.to_be_negative()
test.Expect.to_be_negative()
Asserts the value is less than zero. @returns Expect
Expect.to_be_zero()
test.Expect.to_be_zero()
Asserts the value is zero, of either sign. @returns Expect
Expect.to_be_divisible_by()
test.Expect.to_be_divisible_by(divisor: number) -> Expect
Asserts the value divides by divisor with nothing left over.
Parameters
divisor(number)
Returns Expect
Expect.to_be_even()
test.Expect.to_be_even()
Asserts the value is an even whole number. @returns Expect
Expect.to_be_odd()
test.Expect.to_be_odd()
Asserts the value is an odd whole number. @returns Expect
Expect.to_be_nan()
test.Expect.to_be_nan() -> Expect
Asserts the value is the floating-point not-a-number.
NaN is not == to itself, so this is the only way to check for it.
Returns Expect
Expect.to_be_finite()
test.Expect.to_be_finite()
Asserts the value is a number that is neither infinite nor NaN. @returns Expect
Expect.to_be_infinite()
test.Expect.to_be_infinite()
Asserts the value is positive or negative infinity. @returns Expect
Expect.to_contain()
test.Expect.to_contain(item) -> Expect
Asserts the subject contains item.
What “contains” means follows the subject: a substring of a string, an
element of a list or a bytes buffer, a key of a dictionary. Comparison
is ==; to_contain_equal() is the structural version.
Parameters
item(any)
Returns Expect
Expect.to_contain_equal()
test.Expect.to_contain_equal(item) -> Expect
Asserts the subject contains an element deeply equal to item, which is
what finding an object in a list needs.
Parameters
item(any)
Returns Expect
Expect.to_contain_ignoring_case()
test.Expect.to_contain_ignoring_case(part: string) -> Expect
Asserts the string contains part, ignoring case.
Parameters
part(string)
Returns Expect
Expect.to_equal_ignoring_case()
test.Expect.to_equal_ignoring_case(expected: string) -> Expect
Asserts the string equals expected, ignoring case.
Parameters
expected(string)
Returns Expect
Expect.to_start_with()
test.Expect.to_start_with(prefix: string) -> Expect
Parameters
prefix(string)
Returns Expect
Expect.to_end_with()
test.Expect.to_end_with(suffix: string) -> Expect
Parameters
suffix(string)
Returns Expect
Expect.to_match()
test.Expect.to_match(pattern: string) -> Expect
Asserts the string matches a regular expression.
expect(id).to_match('/^user-[0-9]+$/')
Parameters
pattern(string) — a Zuri regular expression, delimiters included.
Returns Expect
Expect.to_be_blank()
test.Expect.to_be_blank() -> Expect
Asserts the string is empty or contains nothing but whitespace.
Returns Expect
Expect.to_have_length()
test.Expect.to_have_length(expected: int) -> Expect
Asserts the subject’s length.
Parameters
expected(int)
Returns Expect
Expect.to_be_empty()
test.Expect.to_be_empty() -> Expect
Asserts the subject has no elements, no entries, or no characters.
Returns Expect
Expect.to_contain_all()
test.Expect.to_contain_all(items: list) -> Expect
Asserts every one of items is present, in any order.
Parameters
items(list)
Returns Expect
Expect.to_contain_any()
test.Expect.to_contain_any(items: list) -> Expect
Asserts at least one of items is present.
Parameters
items(list)
Returns Expect
Expect.to_contain_none()
test.Expect.to_contain_none(items: list) -> Expect
Asserts none of items is present.
Parameters
items(list)
Returns Expect
Expect.to_contain_exactly()
test.Expect.to_contain_exactly(items: list) -> Expect
Asserts the list holds exactly items, in any order and with the same
number of duplicates.
This is the matcher for a collection whose order is not part of the contract, such as one built from a dictionary’s keys.
Parameters
items(list)
Returns Expect
Expect.to_have_key()
test.Expect.to_have_key(key) -> Expect
Asserts the dictionary has key, whatever it maps to.
Parameters
key(any)
Returns Expect
Expect.to_have_keys()
test.Expect.to_have_keys(keys: list) -> Expect
Asserts the dictionary has every one of keys.
Parameters
keys(list)
Returns Expect
Expect.to_have_value()
test.Expect.to_have_value(value) -> Expect
Asserts the dictionary maps some key to value.
Parameters
value(any)
Returns Expect
Expect.to_have_property()
test.Expect.to_have_property(path: string, value) -> Expect
Asserts something exists at a dotted path, and optionally that it equals
value.
The path walks dictionary keys, instance properties and list indices alike, so one expression reaches into a whole decoded response:
expect(payload).to_have_property('data.items.0.id', 7)
Parameters
path(string)value(?any) — when given, what must be there. Left out, this only asserts that the path resolves to something other thannil.
Returns Expect
Expect.to_be_sorted()
test.Expect.to_be_sorted() -> Expect
Asserts the list is in ascending order.
Numbers compare numerically and strings compare by codepoint. A list mixing the two has no order to be in, and says so.
Returns Expect
Expect.to_have_unique_items()
test.Expect.to_have_unique_items() -> Expect
Asserts no two elements of the list are equal.
Returns Expect
Expect.to_raise()
test.Expect.to_raise() -> Expect
Asserts that calling the subject raises.
The subject must be a function taking no arguments; wrap the call being tested in one.
expect(@{ parse('') }).to_raise()
Returns Expect
Expect.to_raise_instance_of()
test.Expect.to_raise_instance_of(klass: type) -> Expect
Asserts that calling the subject raises an instance of klass, or of
something inheriting from it.
expect(@{ parse('') }).to_raise_instance_of(ValueError)
Parameters
type— klass
Returns Expect
Expect.to_raise_with_message()
test.Expect.to_raise_with_message(part: string) -> Expect
Asserts that calling the subject raises something whose message contains
part, or matches it when part is a regular expression.
expect(@{ withdraw(10, 5) }).to_raise_with_message('cannot withdraw')
expect(@{ withdraw(10, 5) }).to_raise_with_message('/withdraw [0-9]+/')
Parameters
part(string) — taken as a regular expression when it is delimited like one, and as a substring otherwise.
Returns Expect
Expect.to_not_raise()
test.Expect.to_not_raise() -> Expect
Asserts that calling the subject does not raise.
The same as .not.to_raise(), and there for the many tests that read
better stating it directly.
Returns Expect
Expect.to_have_been_called()
test.Expect.to_have_been_called() -> Expect
Asserts the mock has been called at least once.
The subject may be the Mock or the function it handed out.
Returns Expect
Expect.to_have_been_called_times()
test.Expect.to_have_been_called_times(count: int) -> Expect
Asserts the mock has been called exactly count times.
Parameters
count(int)
Returns Expect
Expect.to_have_been_called_with()
test.Expect.to_have_been_called_with(...args: list) -> Expect
Asserts the mock was called at least once with exactly these arguments.
expect(send).to_have_been_called_with('hello', { retry: true })
Parameters
args(...any)
Returns Expect
Expect.to_have_been_last_called_with()
test.Expect.to_have_been_last_called_with(...args: list) -> Expect
Asserts the mock’s most recent call had exactly these arguments.
Parameters
args(...any)
Returns Expect
Expect.to_have_been_nth_called_with()
test.Expect.to_have_been_nth_called_with(index, ...args: list) -> Expect
Asserts the mock’s index-th call had exactly these arguments, counting
from zero. A negative index counts back from the most recent.
Parameters
index(int)args(...any)
Returns Expect
Expect.to_have_returned()
test.Expect.to_have_returned() -> Expect
Asserts at least one call returned rather than raised.
Returns Expect
Expect.to_have_returned_with()
test.Expect.to_have_returned_with(value) -> Expect
Asserts at least one call returned value.
Parameters
value(any)
Returns Expect
Expect.to_have_raised()
test.Expect.to_have_raised() -> Expect
Asserts at least one call raised.
Returns Expect
Expect.to_print()
test.Expect.to_print(part: string) -> Expect
Asserts that running the subject prints something containing part.
The subject must be a function taking no arguments. Everything Zuri
writes to standard output while it runs is collected: echo, print()
and io.stdout alike.
expect(@{ greet('Ada') }).to_print('Hello, Ada')
Parameters
part(string)
Returns Expect
Expect.to_print_exactly()
test.Expect.to_print_exactly(expected: string) -> Expect
Asserts that running the subject prints exactly expected, trailing
newline and all.
Parameters
expected(string)
Returns Expect
Expect.to_print_nothing()
test.Expect.to_print_nothing() -> Expect
Asserts that running the subject prints nothing at all.
Returns Expect
Expect.to_match_snapshot()
test.Expect.to_match_snapshot(name: ?string) -> Expect
Asserts the value still looks the way it did when it was first recorded.
The first run writes the snapshot and passes. Later runs compare against
it. See test.snapshot on where the files live, how
to update them, and why a brand new snapshot fails on CI.
expect(render(invoice)).to_match_snapshot()
expect(headers).to_match_snapshot('response headers')
Parameters
name(?string) — distinguishes several snapshots in one test. Without one they are numbered in the order they are taken.
Returns Expect
Expect.to_satisfy()
test.Expect.to_satisfy(predicate: function, description: ?string) -> Expect
Asserts predicate(value) is truthy, for the assertion no built-in
matcher makes.
expect(port).to_satisfy(@(n) { return n % 2 == 0 }, 'to be an even port')
Parameters
predicate(function)description(?string) — completes the sentenceExpected <value> .... Without one the message says only that a predicate was not satisfied, which is rarely enough.
Returns Expect
test.format
import test.format
testdoes not re-export this module, so it is reached only by importing it directly.
Turning any Zuri value into text a person can read in a failure message.
Two shapes are produced. inline() gives one line, abbreviated to fit
in a sentence: this is what Expected 3 but received 4 is built from.
block() gives a list of lines, indented, with nothing abbreviated
away: this is what a diff and a snapshot are built from.
Both are safe on values that contain themselves. A list or a dictionary
already being rendered further up the same call is printed as
[Circular] rather than followed round again.
Limits
inline() shows at most 8 entries of a list or dictionary, nests at
most 3 levels deep, and shortens a string past 60 characters. Anything
cut off is marked, never silently dropped. Pass an options dictionary to
change any of them:
import test.format
echo format.inline(big_list, { max_items: 40, max_depth: 6 })
block() has no item or depth limit at all, because the reader asked
for the whole thing.
Constants
DEFAULT_MAX_ITEMS
test.format.DEFAULT_MAX_ITEMS = 8
DEFAULT_MAX_DEPTH
test.format.DEFAULT_MAX_DEPTH = 3
DEFAULT_MAX_STRING
test.format.DEFAULT_MAX_STRING = 60
Functions
type_name()
test.format.type_name(value) -> string
The name of value’s type: 'nil', 'bool', 'int', 'float',
'bigint', 'string', 'bytes', 'list', 'dict', 'range',
'function', 'class', 'instance', 'module' or 'file'.
Every instance is an 'instance', whatever it was built from. Use
type_label() for the form that names the class.
Parameters
value(any)
Returns string
type_label()
test.format.type_label(value) -> string
How to name value’s type in a sentence a person reads.
The same as type_name(), except that an instance is called by its
class: “expected a Point, received a Vector” beats hearing that both of
them are instances.
Parameters
value(any)
Returns string
with_article()
test.format.with_article(word: string) -> string
word with the article that belongs in front of it.
Worth the three lines: “expected a int” is the kind of wording that makes a tool feel unfinished.
Parameters
word(string)
Returns string
properties_of()
test.format.properties_of(value) -> list
The names of an instance’s own properties, in declaration order.
The single place in the test module that knows how to look inside an instance, so equality, diffing and rendering never disagree about what an object’s contents are.
Parameters
value(instance)
Returns list
property_of()
test.format.property_of(value, name: string)
Reads one of an instance’s own properties by name.
Parameters
value(instance)name(string)
Returns — any: nil when there is no such property.
class_name()
test.format.class_name(value) -> string
The name of the class value was built from, or nil when it is not an
instance.
Parameters
value(any)
Returns string
inline()
test.format.inline(value, options: ?dict) -> string
value rendered as a single line, abbreviated to stay readable inside a
sentence.
import test.format
echo format.inline({ name: 'Ada', tags: ['x', 'y'] })
{ name: 'Ada', tags: ['x', 'y'] }
Parameters
value(any)options(?dict) — any ofcolor,max_items,max_depth,max_string,sort_keys. Anything left out keeps its default.
Returns string
inline_plain()
test.format.inline_plain(value, options: ?dict) -> string
inline() with colour suppressed, whatever the terminal supports.
This is what a machine-readable reporter and a snapshot key are built from, since an escape sequence in either would be a correctness bug rather than a cosmetic one.
Parameters
value(any)options(?dict)
Returns string
block()
test.format.block(value, options: ?dict)
value rendered over as many lines as it takes, with nothing
abbreviated away.
A scalar comes back as a single-element list, so the caller never has to special-case it.
import test.format
for line in format.block({ id: 7, tags: ['a', 'b'] }) {
echo line
}
{
id: 7,
tags: [
'a',
'b'
]
}
Parameters
value(any)options(?dict) — asinline(), except thatmax_itemsandmax_depthdefault to unlimited.
Returns — list: the lines, without trailing newlines.
serialize()
test.format.serialize(value)
block() with colour suppressed and dictionary keys sorted, which
together make the output stable enough to commit to a repository.
This is the exact text a snapshot file holds.
Parameters
value(any)
Returns — string: the lines joined with \n, with no trailing one.
duration()
test.format.duration(milliseconds: number) -> string
A duration in milliseconds, rendered the way a reader wants to see it: whole milliseconds below a second, and seconds with one decimal place above.
Parameters
milliseconds(number)
Returns string
plural()
test.format.plural(count: int, singular: string, plural: ?string) -> string
count followed by singular or plural, whichever the count calls
for.
Parameters
count(int)singular(string)plural(?string) —singular + 's'by default.
Returns string
test.mock
import test.mock
testlifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledtest.mock.*needsimport test.mock.
Test doubles: functions that record how they were called, and stand-ins that replace a real one for the length of a test.
A recording function
import test { * }
describe('retry', @{
it('calls the operation once per attempt', @{
var operation = mock()
operation.raises_once(Error('first attempt'))
operation.returns('ok')
expect(retry(operation.fn, 3)).to_be('ok')
expect(operation).to_have_been_called_times(2)
})
})
mock() hands back a Mock, and mock.fn is the plain function to
pass wherever a callback is wanted. Matchers accept either, so
expect(operation) and expect(operation.fn) mean the same thing.
Replacing something that already exists
spy_on() swaps a function out in place and gives back a Mock that
both records and stands in for it:
var clock = { now: @{ return real_time() } }
var now = spy_on(clock, 'now')
now.returns(1000)
It works on a dictionary entry and on an instance property that holds a function. It does not work on a class method or a module function: Zuri classes are immutable once declared, and a module’s members cannot be assigned from outside it. Code that wants to be substitutable takes its collaborators as arguments or holds them in properties, which is the shape worth designing for anyway.
Lifetime
The runner calls restore_all() after every test, so a spy never
outlives the test that installed it, even one that failed partway
through. Nothing needs to be undone by hand.
Functions
mock()
test.mock(implementation: ?function, name: ?string) -> Mock
A new recording function.
var send = mock()
notify(send.fn)
expect(send).to_have_been_called_with('hello')
Parameters
implementation(?function) — what it does when called. Without one it records the call and returnsnil.name(?string) — used in failure messages;'mock'by default.
Returns Mock
spy_on()
test.spy_on(target, key: string) -> Mock
Replaces target[key] with a recording stand-in that calls through to
the original, and returns the Mock that now sits there.
Calling through is the default on purpose: a spy that changes behaviour
as well as observing it is a different tool, and saying so is one
.returns() away.
var gateway = { charge: @(amount) { return real_charge(amount) } }
var charge = spy_on(gateway, 'charge')
checkout(gateway)
expect(charge).to_have_been_called_times(1)
Parameters
target(dict|instance) — a dictionary, or an instance with a property holding a function.key(string)
Returns Mock
Raises TestSetupError when target is not a dictionary or an
instance, when it has no such entry, or when that entry does not hold
something callable. A class method cannot be spied on: classes are
immutable once declared.
mock_of()
test.mock.mock_of(value)
The Mock behind value, which may be a Mock already or the plain
function one handed out.
Parameters
value(any)
Returns — Mock: nil when value is neither.
restore_all()
test.mock.restore_all()
Undoes every spy and forgets every mock built so far.
The runner calls this after each test, so a spy installed by a test that failed halfway through still comes back out.
reset_all()
test.mock.reset_all()
Forgets the calls recorded by every live mock, leaving the mocks themselves and their behaviours in place.
Classes
Call
class test.mock.Call
One recorded call.
Fields
| Field | Type | Description |
|---|---|---|
args | The arguments, in order. | |
result | What the call returned, or nil when it raised. | |
error | The error it raised, or nil when it returned. | |
threw | Whether the call raised rather than returned. |
Constructor
test.mock.Call(args)
Mock
class test.Mock
A recording stand-in for a function.
Built by mock() and by spy_on(); there is no reason to construct one
directly.
Fields
| Field | Type | Description |
|---|---|---|
name | What the mock is called in failure messages. | |
fn | The plain function to pass around. | |
calls | Every call so far, oldest first. |
Constructor
test.Mock(name, implementation, replacing)
Parameters
name(?string)implementation(?function) — what the mock does when called. A mock with none returnsnil.replacing(?dict) — for a spy,{ target, key, original }naming what this mock was installed over, sorestore()knows what to put back.
Mock.returns()
test.Mock.returns(value)
Makes every call from now on return value.
Parameters
value(any)
Returns — Mock: itself, so calls chain.
Mock.returns_once()
test.Mock.returns_once(value) -> Mock
Makes the next call return value, once. Queue several to script a
sequence; once the queue runs dry the standing behaviour takes over
again.
Parameters
value(any)
Returns Mock
Mock.raises()
test.Mock.raises(value) -> Mock
Makes every call from now on raise value.
Parameters
value(Error)
Returns Mock
Mock.raises_once()
test.Mock.raises_once(value) -> Mock
Makes the next call raise value, once.
Parameters
value(Error)
Returns Mock
Mock.implements()
test.Mock.implements(implementation: function) -> Mock
Makes every call from now on run implementation and return what it
returns. The implementation receives exactly the arguments the mock was
called with.
Parameters
implementation(function)
Returns Mock
Mock.implements_once()
test.Mock.implements_once(implementation: function) -> Mock
Makes the next call run implementation, once.
Parameters
implementation(function)
Returns Mock
Mock.clear_behaviour()
test.Mock.clear_behaviour() -> Mock
Restores the original behaviour, dropping the standing one and anything still queued. Recorded calls are kept.
Returns Mock
Mock.call_count()
test.Mock.call_count() -> int
How many times the mock has been called.
Returns int
Mock.called()
test.Mock.called() -> bool
Whether the mock has been called at all.
Returns bool
Mock.call_args()
test.Mock.call_args() -> list
The arguments of every call, as a list of lists.
Returns list
Mock.last_call()
test.Mock.last_call() -> Call
The most recent call, or nil when there has not been one.
Returns Call
Mock.nth_call()
test.Mock.nth_call(index: int) -> Call
The index-th call, counting from zero, or nil when there have not
been that many. A negative index counts back from the most recent.
Parameters
index(int)
Returns Call
Mock.reset()
test.Mock.reset() -> Mock
Forgets every recorded call. The behaviour is left alone.
Returns Mock
Mock.restore()
test.Mock.restore() -> Mock
Puts back whatever this mock replaced, if it replaced anything.
Safe to call more than once, and on a mock that was never installed over something.
Returns Mock
test.reporter
import test.reporter
testlifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledtest.reporter.*needsimport test.reporter.
How a run is shown.
The runner knows nothing about output. It walks the tree and calls the methods below as things happen, and a reporter decides what, if anything, that looks like. Swapping one for another changes the report and nothing else.
The built-in reporters
| Name | What it is for |
|---|---|
spec | the default: an indented tree, with failures spelled out |
dot | one character per test, for a run too long to read |
tap | TAP version 14, for anything that already speaks TAP |
junit | JUnit XML, which is what most CI systems ingest |
json | one JSON document at the end, for a tool of your own |
ndjson | one JSON object per event, as it happens |
silent | nothing at all |
test.run({ reporter: 'dot' }) picks one by name, and test.run({ reporter: MyReporter() })
uses your own.
Writing one
Subclass Reporter and override what you care about. Every method has a
do-nothing default, so a reporter that only reacts to failures is six
lines:
import test.reporter { Reporter }
class Quiet < Reporter {
test_finished(test) {
if test.status == 'failed' {
echo test.full_name()
}
}
}
Constants
PREFIX
test.reporter.PREFIX = '@@zuri-test@@'
What marks a line of ndjson output as protocol rather than as
something the test file happened to print.
Functions
create()
test.reporter.create(name: string, options: ?dict) -> Reporter
Builds a reporter by name.
Parameters
name(string) — one ofspec,dot,tap,junit,json,ndjson,silent.options(?dict) — passed to the reporter’s constructor.
Returns Reporter
Raises TestSetupError when there is no such reporter.
Classes
Reporter
class test.Reporter
The interface the runner talks to, with every method doing nothing.
Subclass it and override what you want; there is no need to call
parent() from an override, since nothing here does anything.
Reporter.run_started()
test.Reporter.run_started(plan)
Called once, before anything runs.
Parameters
plan(dict) —{ cases, suites, files, options }, wherecasesis how many tests are going to run.
Reporter.suite_started()
test.Reporter.suite_started(suite)
Called when a suite is entered, before any of its tests.
Parameters
suite(Suite)
Reporter.suite_finished()
test.Reporter.suite_finished(suite)
Called when a suite is left, after every one of its tests.
Parameters
suite(Suite)
Reporter.test_started()
test.Reporter.test_started(test)
Called before a test’s body runs. Not called for a test that is skipped or is a todo.
Parameters
test(Case)
Reporter.test_finished()
test.Reporter.test_finished(test)
Called once per test, whatever became of it, including the ones that never ran.
Parameters
test(Case)
Reporter.run_finished()
test.Reporter.run_finished(summary, root)
Called once, after everything.
Parameters
summary(Summary)root(Suite)
Spec
class test.reporter.Spec < Reporter
The default: the suite tree, one line per test, and every failure written out in full at the end.
Constructor
test.reporter.Spec(options)
Parameters
options(?dict) —slowis the millisecond threshold past which a test’s time is highlighted (300by default,0to never highlight).verboseprints a passing test’s captured output as well as a failing one’s.
Spec.run_started()
test.reporter.Spec.run_started(plan)
Spec.test_finished()
test.reporter.Spec.test_finished(test)
Spec.run_finished()
test.reporter.Spec.run_finished(summary, root)
Dot
class test.reporter.Dot < Reporter
One character per test, wrapped to the terminal, then the same failure detail and summary the spec reporter gives.
For a run long enough that a line per test is more scrolling than information.
Constructor
test.reporter.Dot(options)
Dot.run_started()
test.reporter.Dot.run_started(plan)
Dot.test_finished()
test.reporter.Dot.test_finished(test)
Dot.run_finished()
test.reporter.Dot.run_finished(summary, root)
Tap
class test.reporter.Tap < Reporter
TAP version 14: one ok/not ok line per test, with failure detail in
a YAML block underneath.
The output carries no colour, whatever the terminal supports, since something else is going to parse it.
Constructor
test.reporter.Tap(options)
Tap.run_started()
test.reporter.Tap.run_started(plan)
Tap.test_finished()
test.reporter.Tap.test_finished(test)
Tap.run_finished()
test.reporter.Tap.run_finished(summary, root)
Junit
class test.reporter.Junit < Reporter
JUnit XML, which is the format nearly every CI system knows how to turn into a test report page.
Each describe becomes a <testsuite> and each test a <testcase>.
Written to standard output; redirect it to the file your CI is
configured to collect.
Constructor
test.reporter.Junit(options)
Junit.run_finished()
test.reporter.Junit.run_finished(summary, root)
Json
class test.reporter.Json < Reporter
One JSON document at the end, holding the summary and every test.
For a tool of your own that wants the whole run rather than a stream of it.
Constructor
test.reporter.Json(options)
Json.test_finished()
test.reporter.Json.test_finished(test)
Json.run_finished()
test.reporter.Json.run_finished(summary, root)
Ndjson
class test.reporter.Ndjson < Reporter
One JSON object per line, emitted as each thing happens.
This is what a test file writes when it is being run by the conductor in a process of its own: the parent reads the stream as it arrives rather than waiting for the child to finish.
Every line carries the prefix below, so anything else the file prints can be told apart from the protocol and passed through.
Constructor
test.reporter.Ndjson(options)
Ndjson.run_started()
test.reporter.Ndjson.run_started(plan)
Ndjson.test_finished()
test.reporter.Ndjson.test_finished(test)
Ndjson.run_finished()
test.reporter.Ndjson.run_finished(summary, root)
Silent
class test.reporter.Silent < Reporter
No output at all, for a caller reading the returned Summary instead.
Constructor
test.reporter.Silent(options)
test.result
import test.result
testlifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledtest.result.*needsimport test.result.
The shapes a test run is made of: the tree the declarations build, and what running it produced.
Every reporter is handed these objects and nothing else, so a custom
reporter has the same view of a run that the built-in ones do. See
test.reporter for what a reporter does with them.
Constants
PENDING
test.result.PENDING = 'pending'
A test was declared and not yet run.
PASSED
test.result.PASSED = 'passed'
It ran and every assertion held.
FAILED
test.result.FAILED = 'failed'
It ran and something did not hold.
SKIPPED
test.result.SKIPPED = 'skipped'
It was not run: skip, a filter, or another test’s only.
TODO
test.result.TODO = 'todo'
It was declared with no body, as a note to write it later.
FLAKY
test.result.FLAKY = 'flaky'
It failed, was retried, and then passed.
Classes
Failure
class test.Failure
One thing that went wrong.
A test can carry more than one: a failing body and then a failing
after_each are two separate answers to “what went wrong”, and
collapsing them loses the one that explains the other.
Fields
| Field | Type | Description |
|---|---|---|
kind | Where it came from: 'assertion' for a matcher, 'error' for anything else raised by the test body,… | |
message | The one-line summary. | |
matcher | The matcher that failed, for an assertion. | |
expected | What was expected, when that is a meaningful thing to show. | |
received | What was received. | |
has_values | Whether expected and received are worth printing. | |
details | Pre-rendered lines to print under the message. | |
stacktrace | The raw stack trace, innermost first. |
Constructor
test.Failure(kind, message)
Failure.origin()
test.Failure.origin()
Where the failure happened, in the code under test.
Returns — source.Frame: nil when the trace holds nothing outside
the test module.
Case
class test.Case
One declared test, and what became of it.
Fields
| Field | Type | Description |
|---|---|---|
name | The name given to it(). | |
owner | The suite it was declared in. | |
file | The file it was declared in. | |
mode | How it was declared: 'normal', 'skip', 'only' or 'todo'. | |
body | The body, or nil for a todo. | |
options | Its options, as given to it(). | |
status | What became of it: one of the status constants. | |
failures | Everything that went wrong. | |
duration | How long the last attempt took, in milliseconds. | |
attempts | How many times the body ran, counting retries. | |
output | What it printed, when output was being captured. | |
skip_reason | Why it was skipped, when something other than skip decided. |
Constructor
test.Case(name, owner, file, mode, body, options)
Case.depth()
test.Case.depth()
How deeply nested the test is. @returns int
Case.suite_path()
test.Case.suite_path()
The enclosing suite names, outermost first. @returns list
Case.full_name()
test.Case.full_name() -> string
The test’s full name, suite path included.
Returns string
Case.is_slow()
test.Case.is_slow(threshold: number)
Whether it ran longer than threshold milliseconds. @param number
threshold @returns bool
Case.tags()
test.Case.tags()
The tags it was declared with. @returns list
Suite
class test.Suite
A describe() block: its tests, its nested suites, and its hooks.
Fields
| Field | Type | Description |
|---|---|---|
name | The name given to describe(), or '' for the implicit root. | |
owner | The enclosing suite, or nil for the root. | |
file | The file it was declared in. | |
mode | How it was declared: 'normal', 'skip' or 'only'. | |
children | Its tests and nested suites, in declaration order. | |
before_all | Hooks registered inside it. | |
after_all | ||
before_each | ||
after_each | ||
failures | Anything that went wrong in a hook belonging to this suite. |
Constructor
test.Suite(name, owner, file, mode)
Suite.hooks_for()
test.Suite.hooks_for(kind: string) -> list
The hooks of one kind registered on this suite, in the order they were registered.
Parameters
kind(string) —'before_all','after_all','before_each'or'after_each'.
Returns list
Suite.depth()
test.Suite.depth()
How deeply nested the suite is; the root is 0. @returns int
Suite.path()
test.Suite.path() -> list
This suite’s name preceded by its ancestors’, skipping the unnamed root.
Returns list
Suite.cases()
test.Suite.cases() -> list
Every test in this suite and everything under it, in declaration order.
Returns list
Suite.suites()
test.Suite.suites() -> list
Every suite under this one, itself included.
Returns list
Summary
class test.Summary
What a whole run came to.
Fields
| Field | Type | Description |
|---|---|---|
suites | Suites that contained at least one test that ran. | |
passed | ||
failed | ||
skipped | ||
todo | ||
flaky | Tests that passed only after a retry. | |
failures | Every test that failed, for the report at the end. | |
slow | Tests that ran slower than the slow threshold. | |
duration | Total wall time, in milliseconds. | |
seed | The seed the order was shuffled with, or nil when it was not. | |
snapshots | What happened to snapshots, from test.snapshot. | |
bailed | Whether the run stopped early because bail was reached. |
Constructor
test.Summary()
Summary.ran()
test.Summary.ran()
Tests that actually ran. @returns int
Summary.total()
test.Summary.total()
Every test the run knew about. @returns int
Summary.ok()
test.Summary.ok()
Whether the run should be considered a success. @returns bool
Summary.exit_code()
test.Summary.exit_code()
0 when everything passed, 1 when anything did not. @returns int
Summary.to_dict()
test.Summary.to_dict() -> dict
The run as plain data, for a machine-readable reporter or for a caller driving the framework itself.
Returns dict
Summary.to_string()
test.Summary.to_string() -> string
Returns string
test.runner
import test.runner
testdoes not re-export this module, so it is reached only by importing it directly.
Collecting the tests a file declares, deciding which of them to run, and running them.
Two phases, not one
describe() and it() do not run anything. They build a tree, and
run() walks it afterwards. That separation is what makes only,
filtering, shuffling, bailing and an up-front test count possible at
all: none of them can be decided while the file is still being read.
Declaring the first test registers an os.at_exit() handler that calls
run() once the file is done, so a test file needs no closing line. An
explicit run() marks the tree as run and the handler then does
nothing.
Hook ordering
before_all runs once, immediately before the first test in its suite
that is actually going to run, and not at all for a suite whose tests
were all filtered out. after_all runs after the last one, and only if
before_all ran; it runs even when before_all failed, so whatever the
setup acquired before failing is still let go. before_each runs
outermost first, after_each innermost first, and after_each runs
even when the test failed.
Timeouts
Zuri runs synchronously, so a test runs to completion and is then timed:
the timeout option is measured after the body returns. It catches a
test that got too slow, not one that hangs. A hang needs a process
boundary, which is what the conductor puts around each file; see
test.conduct.
Functions
root()
test.runner.root() -> Suite
The tree as it stands, for a caller that wants to look at what was declared without running it.
Returns Suite
reset()
test.runner.reset()
Throws away every declaration and every snapshot store, so a second run in the same process starts from nothing.
describe()
test.runner.describe(name: string, body: function, mode: ?string) -> Suite
Opens a suite, runs body to collect what is inside it, and closes it
again.
Parameters
name(string)body(function)mode(?string) —'normal','skip'or'only'.
Returns Suite
it()
test.runner.it(name: string, body: ?function, options: ?dict, mode: ?string) -> Case
Declares one test.
Parameters
name(string)body(?function) — left out, the test is a todo.options(?dict) —retries,failing,timeout,tags.mode(?string) —'normal','skip','only'or'todo'.
Returns Case
before_all()
test.runner.before_all(body: function)
Parameters
body(function)
after_all()
test.runner.after_all(body: function)
Parameters
body(function)
before_each()
test.runner.before_each(body: function)
Parameters
body(function)
after_each()
test.runner.after_each(body: function)
Parameters
body(function)
run()
test.runner.run(options: ?dict) -> Summary
Runs everything declared so far and reports it.
import test { * }
describe('parser', @{
it('reads an empty document', @{
expect(parse('')).to_equal({})
})
})
run()
Parameters
options(?dict) — see {test} for every option and its default.
Returns Summary
test.snapshot
import test.snapshot
testdoes not re-export this module, so it is reached only by importing it directly.
Snapshot testing: recording what a value looked like the first time and failing when it stops looking like that.
it('renders the invoice', @{
expect(render(invoice)).to_match_snapshot()
})
The first run writes the value down. Every run after that compares against what was written and shows a diff when it differs. The file is meant to be committed and reviewed like any other code, because that review is the entire value of the technique: an unexplained change to a snapshot in a pull request is exactly the thing worth noticing.
Where they live
Beside the test file, in a __snapshots__ directory, named after it:
tests/invoice.zu gets tests/__snapshots__/invoice.zu.snap.
The file format
Plain text, indented, one entry per snapshot:
# Zuri snapshot file v1
=== Invoice > renders the invoice 1 ===
{
total: 4200
}
The heading is the test’s full name plus the position of the snapshot
within that test, or the name given to to_match_snapshot('name').
Every body line is indented two spaces, which is what keeps a value that
happens to contain === from being read back as a heading.
Updating
Run with ZURI_UPDATE_SNAPSHOTS=1, or pass { update_snapshots: true }
to run(). Every mismatching snapshot is rewritten and reported as
updated rather than failed, and entries nothing asked for any more are
deleted.
On CI
Writing a brand new snapshot is a pass locally and a failure on CI,
because a snapshot nobody has looked at asserts nothing. CI being set
in the environment is what switches this on; { ci: false } turns it
back off.
Constants
HEADER
test.snapshot.HEADER = '# Zuri snapshot file v1'
BODY_INDENT
test.snapshot.BODY_INDENT = ' '
Functions
path_for()
test.snapshot.path_for(test_file: string) -> string
The .snap file that belongs to a given test file.
Parameters
test_file(string)
Returns string
store_for()
test.snapshot.store_for(test_file: string) -> Store
The store for a test file, read from disk the first time it is asked for and kept afterwards.
Parameters
test_file(string)
Returns Store
updating()
test.snapshot.updating() -> bool
Whether snapshots are being rewritten rather than checked.
Returns bool
set_updating()
test.snapshot.set_updating(on: bool)
Turns snapshot rewriting on or off.
Parameters
on(bool)
strict()
test.snapshot.strict() -> bool
Whether a brand new snapshot counts as a failure, which is what it should be anywhere nobody is going to look at it before it is committed.
Returns bool
set_strict()
test.snapshot.set_strict(on: bool)
Turns the strict, new-snapshots-fail behaviour on or off.
Parameters
on(bool)
check()
test.snapshot.check(test_file: string, key: string, serialized: string)
Compares one value against what the snapshot file holds for key,
writing it down when there is nothing there yet.
Parameters
test_file(string) — the file the test was declared in.key(string)serialized(string) — the value, already rendered byformat.serialize().
Returns — dict: { status, expected, received }, where status is
one of 'match', 'written', 'updated', 'mismatch' or 'missing'.
'missing' is the strict-mode answer to what would otherwise have been
'written'.
recorded_lines()
test.snapshot.recorded_lines(test_file: string, key: string) -> list
The lines of the snapshot at key, as a list, for a reporter that wants
to show what was recorded.
Parameters
test_file(string)key(string)
Returns list
flush()
test.snapshot.flush(prune: ?bool)
Writes every changed store back to disk, and in update mode drops entries the run never asked about.
The runner calls this once, after everything has run. Pruning earlier would delete the snapshots of tests that a filter happened to skip.
Parameters
prune(?bool) — whether to drop unused entries. Defaults to whateverupdating()says.
summary()
test.snapshot.summary()
What happened to snapshots over the whole run.
Returns — dict: matched, written, updated, obsolete (found
and left alone), removed (found and deleted, in update mode), and
missing when strict mode refused to write a new one.
reset()
test.snapshot.reset()
Forgets every loaded store and zeroes the counters, so a second run in the same process starts clean.
Classes
Store
class test.snapshot.Store
Every snapshot recorded for one test file, and the file they live in.
Fields
| Field | Type | Description |
|---|---|---|
path | The .snap file this reads and writes. | |
entries | Snapshot key to recorded text. | |
used | Keys this run actually asked about. | |
dirty | Whether anything has changed since it was read. |
Constructor
test.snapshot.Store(path)
Store.read()
test.snapshot.Store.read()
Reads the file, if it is there. A missing file is not an error: it is what every snapshot’s first run looks like.
Store.save()
test.snapshot.Store.save()
Writes the file back, creating the __snapshots__ directory if it is
not there yet. Does nothing when nothing changed.
An empty store deletes its file rather than leaving an unexplained stub behind.
Store.prune()
test.snapshot.Store.prune() -> int
Drops every entry this run never asked about, and reports how many there were.
Returns int
Store.obsolete()
test.snapshot.Store.obsolete() -> list
Keys the file holds that this run never asked about.
Returns list
test.source
import test.source
testdoes not re-export this module, so it is reached only by importing it directly.
Working out where a failure came from, and showing the code that was there.
Every Zuri error carries a stacktrace: a list of strings shaped like
/path/to/file.zu:12 -> divide(). That is enough to point a reader at
the exact line, provided the frames belonging to the test module itself
are dropped first. Nobody needs to be told that the failure passed
through _check() on its way out.
Functions
parse_frame()
test.source.parse_frame(line: string)
Splits one stack trace line into a Frame.
Parameters
line(string)
Returns — Frame: nil when the line is not shaped like a frame,
which is what a trace from a future runtime might well look like.
user_frames()
test.source.user_frames(stacktrace: list)
Every frame of stacktrace that belongs to the code under test,
innermost first.
The test module’s own frames are dropped, because a failure is never interesting for having gone through a matcher.
Parameters
stacktrace(list) — an error’sstacktrace.
Returns — list: a list of Frame.
origin()
test.source.origin(stacktrace: list)
The innermost frame of stacktrace outside the test module: where the
failing line actually is.
Parameters
stacktrace(list)
Returns — Frame: nil when every frame belongs to the framework,
which happens when a matcher is misused rather than failing.
short_path()
test.source.short_path(path: string) -> string
A path written relative to the working directory when it is under it, and left alone when it is not.
Parameters
path(string)
Returns string
code_frame()
test.source.code_frame(frame, context: ?int)
The source around a failure, with the offending line marked.
10 | var total = 0
11 |
> 12 | expect(total).to_be(1)
13 | })
Parameters
frame(Frame)context(?int) — lines of surrounding code either side,2by default.
Returns — list: the lines, already coloured. Empty when the file cannot be read, which is the normal answer for a frame from somewhere that no longer exists.
clear_cache()
test.source.clear_cache()
Forgets every file read for a code frame.
Only matters to a long-lived process that edits the files it is testing between runs.
Classes
Frame
class test.source.Frame
One frame of a stack trace, pulled apart.
Fields
| Field | Type | Description |
|---|---|---|
file | The file, as an absolute path. | |
line | The line number. | |
name | The function, without its trailing (). | |
raw | The frame exactly as the trace spelled it. |
Constructor
test.source.Frame(file, line, name, raw)
Frame.to_string()
test.source.Frame.to_string() -> string
The frame written for a report: a path relative to the working directory, since an absolute one is mostly noise the reader already knows.
Returns string
test.style
import test.style
testdoes not re-export this module, so it is reached only by importing it directly.
Terminal presentation for the test module: colour, symbols, width, and the padding helpers that keep a report’s columns lined up once escape sequences are in the text.
Colour and symbols are decided once, when this module loads, from the
environment the process actually has. Nothing else in the test module
asks whether colour is on; it calls green() and gets plain text back
when it isn’t.
Deciding whether to use colour
In order, the first of these that applies wins:
NO_COLORset to anything non-empty turns colour off, per https://no-color.org. 2.FORCE_COLORset to anything other than0turns it on, which is how CI systems that capture output but still render ANSI ask for it. 3. Standard output being a terminal turns it on, and a pipe or a file turns it off.
set_enabled() overrides the lot, for a caller that has already made
the decision itself.
Deciding whether to use Unicode
ZURI_TEST_ASCII set to anything non-empty forces the ASCII symbol set.
Otherwise the box-drawing and tick/cross characters are used everywhere
except a Windows console that is not Windows Terminal, which is the one
common environment that still renders them as mojibake.
Functions
enabled()
test.style.enabled() -> bool
Whether colour is currently being emitted.
Returns bool
set_enabled()
test.style.set_enabled(on: bool)
Forces colour on or off, overriding what the environment said.
Parameters
on(bool)
unicode()
test.style.unicode() -> bool
Whether the Unicode symbol set is in use, as opposed to the ASCII fallbacks.
Returns bool
set_unicode()
test.style.set_unicode(on: bool)
Forces the Unicode symbol set on or off.
Parameters
on(bool)
bold()
test.style.bold(text: string) -> string
Emphasis. Returns text unchanged when colour is off.
Parameters
text(string)
Returns string
dim()
test.style.dim(text: string) -> string
De-emphasis, for detail that should recede: timings, counts, the suite path above a failure.
Parameters
text(string)
Returns string
inverse()
test.style.inverse(text: string) -> string
Reserved for text that has to be found instantly on a busy screen.
Parameters
text(string)
Returns string
red()
test.style.red(text: string)
Failure. @param string text @returns string
green()
test.style.green(text: string)
Success. @param string text @returns string
yellow()
test.style.yellow(text: string)
Skipped and other deliberate non-results. @param string text @returns string
cyan()
test.style.cyan(text: string)
Structure: suite names, headings. @param string text @returns string
blue()
test.style.blue(text: string)
Todo entries and other informational notes. @param string text @returns string
magenta()
test.style.magenta(text: string)
Values in a rendered diff or failure message. @param string text @returns string
grey()
test.style.grey(text: string)
Punctuation and separators. @param string text @returns string
white()
test.style.white(text: string)
Plain foreground, used to lift a key out of dimmed surroundings. @param string text @returns string
badge()
test.style.badge(kind: string, text: string) -> string
A filled, inverted label, the way a status banner reads in a CI log.
kind is one of 'pass', 'fail', 'skip', 'todo' or 'flaky';
anything else gets the neutral grey background.
Parameters
kind(string)text(string)
Returns string
symbols()
test.style.symbols() -> dict
The symbol set in use, keyed by role.
Every entry is a single display column wide in both sets, so a layout
built on them does not shift when the ASCII fallbacks are in play. The
exception is arrow, which is two columns in ASCII.
Returns dict
strip()
test.style.strip(text: string) -> string
text with every ANSI escape sequence removed.
Parameters
text(string)
Returns string
visible_length()
test.style.visible_length(text: string) -> int
How many columns text occupies once its escape sequences are
discounted. This, not length(), is what padding and alignment have to
measure.
Parameters
text(string)
Returns int
Note: counts codepoints, so a wide CJK character or an emoji is counted as one column even though a terminal draws it as two. Test names are the only user-supplied text this is applied to.
pad_right()
test.style.pad_right(text: string, width: int, fill: ?string) -> string
Pads text on the right to width visible columns, leaving it alone
when it is already that wide or wider.
Parameters
text(string)width(int)fill(?string) — a single character,' 'by default.
Returns string
pad_left()
test.style.pad_left(text: string, width: int, fill: ?string) -> string
Pads text on the left to width visible columns, leaving it alone
when it is already that wide or wider.
Parameters
text(string)width(int)fill(?string) — a single character,' 'by default.
Returns string
truncate()
test.style.truncate(text: string, width: int) -> string
Shortens text to at most width visible columns, marking the cut with
an ellipsis. Returns text untouched when it already fits.
Parameters
text(string)width(int)
Returns string
Note: only safe on text with no escape sequences in it, since it cuts by codepoint and would leave a colour sequence unterminated. Colour the result, not the input.
rule()
test.style.rule(character: string, count: int) -> string
count copies of character.
Parameters
character(string)count(int)
Returns string
indent()
test.style.indent(depth: int) -> string
Two spaces per level, the indent every nested suite and test line in the report is built from.
Parameters
depth(int)
Returns string
width()
test.style.width() -> int
The width to lay the report out to.
Asks the terminal first, falls back to COLUMNS, and finally to a
conventional 80. Clamped to 40..120 so neither a one-column pty nor a
maximised ultrawide produces an unreadable layout.
Returns int
net
import net
The net module provides networking primitives for Transmission Control
Protocol and User Datagram Protocol communication as well as IP address
manipulation interfaces.
TcpStreamcommunicates over TCP.UdpSocketcommunicates over UDP.UnixStreamcommunicates over a unix domain socket, which is a path on the filesystem rather than an address on the network.Ipparses, formats and classifies IP addresses.SocketAddrpairs an IP address with a port.net.tlswraps aTcpStreamin Transport Layer Security.net.dtlsprovides Datagram Transport Layer Security over UDP.net.pollwaits on many sockets at once.net.resolverasks the domain name system questions of its own.
The net API
Every public name in net, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
net.Acceptor | class | An accept loop that never parks the thread inside accept(). |
net.ERROR | constant | Readiness only: the socket has failed, or its handle can no longer supply a descriptor because it was closed,… |
net.HANGUP | constant | Readiness only: the peer has closed its end. |
net.Ip | class | Either an Ipv4 or an Ipv6, for code that needs to accept and work with addresses of either family without… |
net.Ipv4 | class | An IPv4 address, stored internally as four octets in network (i.e. big-endian, most significant octet first)… |
net.Ipv6 | class | An IPv6 address, stored internally as eight 16-bit segments in network (i.e. big-endian, most significant… |
net.Poller | class | A set of sockets to watch, each under a token of your choosing. |
net.READABLE | constant | Interest: tell me when this socket has data to read, a connection to accept, or an end-of-stream to observe. |
net.Shutdown | class | Used with TcpStream.shutdown() to select which half (or both halves) of a full-duplex TCP connection to… |
net.SocketAddr | class | Either a SocketAddrV4 or a SocketAddrV6, for code that needs to accept and work with socket addresses of… |
net.SocketAddrV4 | class | An IPv4 address and port, e.g. 192.168.1.10:8080. |
net.SocketAddrV6 | class | An IPv6 address and port, e.g. [2001:db8::1]:8080, optionally carrying a zone/scope identifier (RFC 4007)… |
net.TcpStream | class | A TCP socket, in either its connected-stream or listening-server role. |
net.UdpSocket | class | A UDP socket, usable for both sending and receiving datagrams, either to/from a single fixed peer (after… |
net.UnixStream | class | A unix domain socket, connected or listening. |
net.WRITABLE | constant | Interest: tell me when this socket can be written to without blocking. |
net.dtls.DtlsConfig | class | The trust roots and optional certificate a DTLS handshake needs - the same idea as tls.TlsConfig, minus the… |
net.dtls.DtlsSocket | class | A DTLS socket, in either its connected (client or accepted-server) role or its listening role. |
net.is_supported | function | Whether unix domain sockets work on this machine. |
net.pair | function | Two sockets already connected to each other, with no path and nothing on the filesystem. |
net.resolve | function | Resolves address - a host:port string whose host may be a name or an IP literal - into every socket… |
net.resolver.CLASSES | constant | The classes a question can be asked in. |
net.resolver.Certification | class | A CAA record: which certificate authorities a domain allows to issue for it. |
net.resolver.MailExchange | class | One mail server for a domain, as an MX record names it. |
net.resolver.MalformedResponse | class | Something came back that is not a DNS message, or is one that contradicts itself. |
net.resolver.NameError | class | The name does not exist. |
net.resolver.NoServersError | class | The resolver has nowhere to send a query: no servers were given and the system has none configured. |
net.resolver.Question | class | The question a message asks, echoed back in the answer so a reply can be matched to what it replies to. |
net.resolver.RCODES | constant | What a server said about the question, by name. |
net.resolver.Record | class | One resource record: a name, what kind of thing it holds, how long it may be cached, and the thing itself. |
net.resolver.Resolver | class | Asks questions of the domain name system and reads the answers. |
net.resolver.ResolverError | class | The root of every error this module raises. |
net.resolver.ResolverTimeout | class | No server answered in time. |
net.resolver.Response | class | A whole answer: what was asked, what came back, and what the server said about it. |
net.resolver.ServerError | class | The server answered, and the answer was a refusal or a failure of its own. |
net.resolver.Service | class | Where a service lives, as an SRV record names it: the host and port, with the priority and weight that… |
net.resolver.StartOfAuthority | class | The authority record at the top of a zone, which says who publishes it and how long the rest of the world may… |
net.resolver.TYPES | constant | The record types this module knows how to read, by name. |
net.resolver.a | function | The IPv4 addresses of a name. |
net.resolver.aaaa | function | The IPv6 addresses of a name. |
net.resolver.addresses | function | Every address a name has, IPv4 first. |
net.resolver.build_query | function | Builds the bytes of a query. |
net.resolver.caa | function | Which certificate authorities a domain allows to issue for it. |
net.resolver.cname | function | The name this one is an alias for, or nil. |
net.resolver.exchange | function | Asks one server one question and returns what it said. |
net.resolver.flush | function | Empties the shared resolver’s cache. |
net.resolver.mx | function | The mail servers for a domain, lowest preference first. |
net.resolver.ns | function | The nameservers a domain is delegated to. |
net.resolver.parse_message | function | Turns the bytes of a message into a Response. |
net.resolver.ptr | function | The names an address points back at. |
net.resolver.query | function | Asks the shared resolver about one name and hands back the whole answer. |
net.resolver.resolve | function | The records of one type published for a name, through the shared resolver. |
net.resolver.reverse_name | function | The name an address’s records are published under. |
net.resolver.shared | function | The resolver the module-level functions use: one shared Resolver built from this machine’s own… |
net.resolver.soa | function | The authority record at the top of a name’s zone, or nil. |
net.resolver.srv | function | The hosts running a service, by priority and then by weight. |
net.resolver.system_search | function | The domain suffixes to try against a name that has no dots in it. |
net.resolver.system_servers | function | The nameservers this machine is configured to use, in the order the system lists them. |
net.resolver.txt | function | The text records of a name. |
net.resolver.use | function | Replaces the shared resolver, so that every module-level function goes through the one given instead. |
net.tls.PeerCertificate | class | The subset of a peer’s certificate that’s useful to inspect from Zuri code, without needing a full… |
net.tls.TlsConfig | class | The handful of choices a TLS handshake needs: which certificate authorities to trust, an optional certificate… |
net.tls.TlsStream | class | An established TLS connection, wrapping an already-connected TcpStream. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
net.addr | net.* | Socket addresses: an IP address and a port. |
net.dtls | net.dtls.* | Datagram Transport Layer Security over a UdpSocket - the same certificate-based encryption and… |
net.ip | net.* | This module provides types for parsing, formatting, comparing and classifying IP addresses in both their IPv4… |
net.poll | net.* | Readiness polling: given a set of sockets, which of them can be read or written right now, without blocking. |
net.resolver | net.resolver.* | A DNS resolver, written in Zuri from the wire format up. |
net.tcp | net.* | This module provides a complete implementation of the Transmission Control Protocol as specified in IETF RFC… |
net.tls | net.tls.* | Transport Layer Security over a TcpStream, built on top of rustls. |
net.udp | net.* | This module provides a complete implementation of the User Datagram Protocol as specified in IETF RFC 768. |
net.unix | net.* | Unix domain sockets: the same stream interface as tcp, over a path on the filesystem rather than an address… |
2026, Richard Ore and Zuri contributors
net.addr
import net
Everything here is re-exported by
net, soimport netis enough and the names are called asnet.*. Importingnet.addron its own works too and reaches the same definitions.
Socket addresses: an IP address and a port. SocketAddrV4 and
SocketAddrV6 each represent an address of their respective family,
while SocketAddr is a small wrapper around either one for code that
needs to accept and work with both interchangeably just like the same
relationship Ip has to Ipv4/Ipv6.
An IPv6 address may also carry a zone/scope identifier (RFC 4007), used
to disambiguate a link-local address between network interfaces, e.g.
fe80::1%3.
TcpStream, UdpSocket, and DtlsSocket return these from
local_address()/peer_address().
import net
var server = net.TcpStream()
server.bind('0.0.0.0:8080')
var local = server.local_address()
echo 'listening on ${local.ip()}, port ${local.port()}'
Classes
SocketAddrV4
class net.SocketAddrV4
An IPv4 address and port, e.g. 192.168.1.10:8080.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.SocketAddrV4(address, port)
Returns a new instance of a SocketAddrV4.
Parameters
address(Ipv4) — The IP addressport(number) — The port,0-65535
Raises Error if address is not an {Ipv4}, or port is missing
or outside 0-65535
SocketAddrV4.parse()
net.SocketAddrV4.parse(address) -> SocketAddrV4
Parses an address given as ip:port, e.g. '192.168.1.10:8080'.
Parameters
address(string) — The address to parse
Returns SocketAddrV4
Raises Error if address isn’t a well-formed IPv4 address
followed by : and a port from 0 to 65535
SocketAddrV4.ip()
net.SocketAddrV4.ip() -> Ipv4
The IP address.
Returns Ipv4
SocketAddrV4.port()
net.SocketAddrV4.port() -> number
The port.
Returns number
SocketAddrV4.equals()
net.SocketAddrV4.equals(other) -> bool
Whether other refers to the same address and port as this one.
Parameters
other(SocketAddrV4) — The address to compare against
Returns bool
SocketAddrV4.to_string()
net.SocketAddrV4.to_string() -> string
This address as ip:port, e.g. '192.168.1.10:8080'.
Returns string
SocketAddrV6
class net.SocketAddrV6
An IPv6 address and port, e.g. [2001:db8::1]:8080, optionally carrying
a zone/scope identifier (RFC 4007) that disambiguates a link-local
address between network interfaces, e.g. [fe80::1%3]:8080.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.SocketAddrV6(address, port, scope_id)
Returns a new instance of a SocketAddrV6.
Parameters
address(Ipv6) — The IP addressport(number) — The port,0-65535scope_id(?number) — The zone/scope identifier, or0(the default) if this address isn’t scoped to a particular interface
Raises Error if address is not an {Ipv6}, port is missing or
outside 0-65535, or scope_id is negative
SocketAddrV6.parse()
net.SocketAddrV6.parse(address) -> SocketAddrV6
Parses an address given as [ip]:port or, for a scoped address,
[ip%scope_id]:port, e.g. '[::1]:8080' or '[fe80::1%3]:8080'.
Parameters
address(string) — The address to parse
Returns SocketAddrV6
Raises Error if address isn’t a well-formed bracketed IPv6
address followed by : and a port from 0 to 65535
SocketAddrV6.ip()
net.SocketAddrV6.ip() -> Ipv6
The IP address.
Returns Ipv6
SocketAddrV6.port()
net.SocketAddrV6.port() -> number
The port.
Returns number
SocketAddrV6.scope_id()
net.SocketAddrV6.scope_id() -> number
The zone/scope identifier, or 0 if this address isn’t scoped to a
particular network interface.
Returns number
SocketAddrV6.equals()
net.SocketAddrV6.equals(other) -> bool
Whether other refers to the same address, port, and scope as this one.
Parameters
other(SocketAddrV6) — The address to compare against
Returns bool
SocketAddrV6.to_string()
net.SocketAddrV6.to_string() -> string
This address as [ip]:port, or [ip%scope_id]:port if it carries a
non-zero scope id.
Returns string
SocketAddr
class net.SocketAddr
Either a SocketAddrV4 or a
SocketAddrV6, for code that needs to accept and work
with socket addresses of either family without committing to one
upfront.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.SocketAddr(inner)
Wraps an already-constructed SocketAddrV4 or
SocketAddrV6. Application code should generally
prefer SocketAddr.parse(), SocketAddr.v4(), or SocketAddr.v6()
over calling this directly.
Parameters
inner(SocketAddrV4|SocketAddrV6) — The address to wrap
SocketAddr.v4()
net.SocketAddr.v4(address, port) -> SocketAddr
Wraps an IPv4 address and port as a SocketAddr.
Parameters
address(Ipv4) — The IP addressport(number) — The port,0-65535
Returns SocketAddr
SocketAddr.v6()
net.SocketAddr.v6(address, port, scope_id) -> SocketAddr
Wraps an IPv6 address and port as a SocketAddr.
Parameters
address(Ipv6) — The IP addressport(number) — The port,0-65535scope_id(?number) — The zone/scope identifier, or0
Returns SocketAddr
SocketAddr.parse()
net.SocketAddr.parse(address) -> SocketAddr
Parses address as either an IPv4 or IPv6 socket address, choosing the
family based on whether it starts with [.
Parameters
address(string) — The address to parse, e.g.'192.168.1.10:8080'or'[::1]:8080'
Returns SocketAddr
Raises Error if address is not a well-formed IPv4 or IPv6 socket
address
SocketAddr.is_v4()
net.SocketAddr.is_v4() -> bool
Whether this is a SocketAddrV4.
Returns bool
SocketAddr.is_v6()
net.SocketAddr.is_v6() -> bool
Whether this is a SocketAddrV6.
Returns bool
SocketAddr.as_v4()
net.SocketAddr.as_v4() -> ?SocketAddrV4
Returns the wrapped SocketAddrV4, or nil if this is
an IPv6 address.
Returns ?SocketAddrV4
SocketAddr.as_v6()
net.SocketAddr.as_v6() -> ?SocketAddrV6
Returns the wrapped SocketAddrV6, or nil if this is
an IPv4 address.
Returns ?SocketAddrV6
SocketAddr.ip()
net.SocketAddr.ip() -> Ip
The IP address, wrapped as an Ip so it can be worked
with without committing to a family.
Returns Ip
SocketAddr.port()
net.SocketAddr.port() -> number
The port.
Returns number
SocketAddr.equals()
net.SocketAddr.equals(other) -> bool
Whether other refers to the same address as this one. An IPv4 address
and an IPv6 address are never equal, even if one is the IPv4-mapped form
of the other.
Parameters
other(SocketAddr) — The address to compare against
Returns bool
SocketAddr.to_string()
net.SocketAddr.to_string() -> string
This address in its family’s standard notation.
Returns string
2026, Richard Ore and Zuri contributors
net.dtls
import net
netexposes this asnet.dtls, soimport netis enough and the names are called asnet.dtls.*.import net.dtlsreaches the same definitions directly.
Datagram Transport Layer Security over a UdpSocket - the same
certificate-based encryption and authentication tls provides, for
protocols built on UDP instead of TCP.
Unlike tls, DtlsSocket owns its own socket rather than wrapping one
you’ve already opened: a UDP port has no OS-level concept of a per-peer
connection, so a single bound DtlsSocket multiplexes every accepted
peer itself, by remote address.
A client:
import net.dtls
var config = dtls.DtlsConfig()
var socket = dtls.DtlsSocket()
socket.connect(config, '203.0.113.10:5684')
socket.write('hello')
echo socket.read(1024)
socket.close()
A server, accepting many clients on one bound port:
import net.dtls
var config = dtls.DtlsConfig()
config.set_cert_chain(cert_chain_pem, private_key_pem)
var server = dtls.DtlsSocket()
server.bind('0.0.0.0:5684')
while true {
var client = server.accept(config)
echo 'connection from ${client.peer_address()}'
client.write('hello over DTLS')
}
Classes
DtlsConfig
class net.dtls.DtlsConfig
The trust roots and optional certificate a DTLS handshake needs - the
same idea as tls.TlsConfig, minus the TLS-specific ALPN/version
options DTLS doesn’t have.
Constructor
net.dtls.DtlsConfig()
Returns a new DtlsConfig with sensible defaults: the bundled Mozilla
root certificate list and no client/server certificate.
DtlsConfig.native_ptr()
net.dtls.DtlsConfig.native_ptr() -> Ptr
Hands out this config’s underlying native handle - DtlsSocket needs it
and, like any _-prefixed field, _ptr isn’t otherwise reachable from
outside this class.
Returns Ptr
DtlsConfig.set_root_store()
net.dtls.DtlsConfig.set_root_store(mode)
Chooses where trusted root certificate authorities come from. Same
meaning as tls.TlsConfig.set_root_store().
Parameters
mode(string) —'bundled'or'native'
Raises Error if mode is neither, or (for 'native') if the OS
trust store can’t be read
DtlsConfig.add_ca_pem()
net.dtls.DtlsConfig.add_ca_pem(pem)
Adds one or more PEM-encoded certificates to this config’s trusted
roots. Same meaning as tls.TlsConfig.add_ca_pem().
Parameters
pem(string) — One or more PEM-encoded CA certificates
Raises Error if none of the certificates in pem could be parsed
DtlsConfig.set_cert_chain()
net.dtls.DtlsConfig.set_cert_chain(cert_chain_pem, key_pem)
Sets the certificate chain and private key this side presents. Required
before DtlsSocket.accept() can be used with this config.
Parameters
cert_chain_pem(string) — The end-entity certificate followed by any intermediate certificates, all PEM-encodedkey_pem(string) — The PEM-encoded private key matching the end-entity certificate
Raises Error if either PEM can’t be parsed, or if the key doesn’t
match the certificate
DtlsConfig.require_client_cert()
net.dtls.DtlsConfig.require_client_cert(required)
For a server config: whether to require the connecting peer to present a trusted certificate (mutual TLS).
Parameters
required(bool)
DtlsConfig.set_insecure()
net.dtls.DtlsConfig.set_insecure(insecure)
Disables certificate verification entirely. Same meaning, and same
“development and testing only” caveat, as
tls.TlsConfig.set_insecure().
Parameters
insecure(bool)
DtlsSocket
class net.dtls.DtlsSocket
A DTLS socket, in either its connected (client or accepted-server) role or its listening role.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.dtls.DtlsSocket(_ptr)
Returns a new, unbound DtlsSocket. Call bind() to listen for
incoming connections, or connect() to dial out to one.
DtlsSocket.bind()
net.dtls.DtlsSocket.bind(address)
Binds this socket to address and prepares it to accept incoming DTLS
associations via accept().
Parameters
address(string|SocketAddr) — The host:port to bind and listen on
Raises Error if binding fails (e.g. the address is already in use)
DtlsSocket.connect()
net.dtls.DtlsSocket.connect(config, address, server_name)
Opens a DTLS association with address, performing the full handshake
before returning. server_name is used to verify the peer’s
certificate, the same way tls.TlsStream.connect() uses it; omit it to
fall back to verifying against the resolved IP address instead of a
hostname.
Parameters
config(DtlsConfig) — The trust roots and options to handshake withaddress(string|SocketAddr) — The host:port to connect toserver_name(?string) — The hostname to verify the peer’s certificate against
Raises Error if resolution or the handshake fails, or the peer’s
certificate isn’t trusted or doesn’t match server_name
DtlsSocket.accept()
net.dtls.DtlsSocket.accept(config) -> DtlsSocket
Waits for the next peer to complete a handshake and returns a new
DtlsSocket representing that specific peer’s association. Only valid
on a bound socket. Blocks (subject to set_read_timeout()) until a new
peer finishes handshaking; existing peers’ traffic keeps flowing to
their own DtlsSocket instances in the meantime.
Parameters
config(DtlsConfig) — Must have a certificate chain set viaDtlsConfig.set_cert_chain()
Returns DtlsSocket — a socket for the newly accepted peer
Raises Error if this socket isn’t bound, config has no
certificate chain, or (with require_client_cert()) a peer’s handshake
didn’t present a trusted certificate
DtlsSocket.read()
net.dtls.DtlsSocket.read(length) -> bytes
Reads the next decrypted application-data message, up to length bytes
of it. DTLS preserves message boundaries the way UDP does: this always
returns one whole message (truncated to length if it was longer),
never a partial one glued to the next.
Parameters
length(number) — The maximum number of bytes to return
Returns bytes — the next message, up to length bytes
Raises Error on an underlying I/O or DTLS record error, or if the
configured read timeout elapses first
DtlsSocket.write()
net.dtls.DtlsSocket.write(data) -> number
Encrypts and sends data as a single application-data message to the
peer this socket is connected/accepted for.
Parameters
data(bytes|string) — The bytes (or UTF-8 text) to send
Returns number — the number of bytes sent
Raises Error on an underlying I/O or DTLS record error
DtlsSocket.local_address()
net.dtls.DtlsSocket.local_address() -> SocketAddr
The local socket address this instance is bound to.
Returns SocketAddr
Raises Error if this socket isn’t bound
DtlsSocket.peer_address()
net.dtls.DtlsSocket.peer_address() -> SocketAddr
The remote peer’s socket address.
Returns SocketAddr
Raises Error if this socket isn’t a connected/accepted association
DtlsSocket.set_read_timeout()
net.dtls.DtlsSocket.set_read_timeout(timeout)
Sets how long read() may block before raising a timeout error. Pass
0 (or any negative number) to block indefinitely, which is the
default.
Parameters
timeout(number) — Milliseconds, or a non-positive number to disable the timeout
DtlsSocket.close()
net.dtls.DtlsSocket.close()
Releases this socket. Safe to call more than once. Only closes this
particular peer association’s bookkeeping - if this is a listening
socket, other already-accepted DtlsSocket instances sharing its
underlying bound port are unaffected.
DtlsSocket.peer_certificate()
net.dtls.DtlsSocket.peer_certificate() -> ?PeerCertificate
The peer’s end-entity certificate, or nil if the peer didn’t present
one.
Returns ?PeerCertificate
DtlsSocket.to_string()
net.dtls.DtlsSocket.to_string()
2026, Richard Ore and Zuri contributors
net.ip
import net
Everything here is re-exported by
net, soimport netis enough and the names are called asnet.*. Importingnet.ipon its own works too and reaches the same definitions.
This module provides types for parsing, formatting, comparing and classifying IP addresses in both their IPv4 and IPv6 forms.
Ipv4 and Ipv6 each represent an address of their respective family,
while Ip is a small wrapper around either one for code that needs to
accept and work with both interchangeably.
The example below parses an address of unknown family and reports a few things about it.
import net
var addr = net.Ip.parse('192.168.1.10')
if addr.is_private() {
echo '${addr} is a private address'
}
Ipv4 and Ipv6 can also be worked with directly when the family is
already known, as shown below.
import net
var a = net.Ipv4.parse('10.0.0.1')
var b = net.Ipv4(10, 0, 0, 2)
echo a.is_private()
echo a.equals(b)
echo a.octets()
Classes
Ipv4
class net.Ipv4
An IPv4 address, stored internally as four octets in network (i.e. big-endian, most significant octet first) order.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.Ipv4(a, b, c, d)
Returns a new instance of an Ipv4 from its four octets, given
in the same left-to-right order they’d be written in dotted-decimal
notation, e.g. Ipv4(192, 168, 1, 10) for 192.168.1.10.
Parameters
a(number) — The 1st octet,0-255b(number) — The 2nd octet,0-255c(number) — The 3rd octet,0-255d(number) — The 4th octet,0-255
Raises Error if any octet is missing or outside 0-255
Ipv4.unspecified()
net.Ipv4.unspecified() -> Ipv4
The 0.0.0.0 address, used to mean “any local address” when binding a
socket.
Returns Ipv4
Ipv4.localhost()
net.Ipv4.localhost() -> Ipv4
The 127.0.0.1 loopback address.
Returns Ipv4
Ipv4.broadcast()
net.Ipv4.broadcast() -> Ipv4
The 255.255.255.255 limited broadcast address.
Returns Ipv4
Ipv4.parse()
net.Ipv4.parse(address) -> Ipv4
Parses an address given in standard dotted-decimal notation, e.g.
'192.168.1.10'.
Parameters
address(string) — The address to parse
Returns Ipv4
Raises Error if address is not four dot-separated octets, each a
decimal number from 0 to 255
Note: Each octet must be written without a leading zero (other than the literal digit
0itself) -'192.168.001.10'is rejected rather than guessed at, since a leading zero is read as an octal prefix by some other tools and silently accepting it here could make an address mean two different things depending on what reads it.
Ipv4.octets()
net.Ipv4.octets() -> number[]
Returns the four octets making up this address as a list, in the same left-to-right order they’d be written in dotted-decimal notation. Each call returns a fresh list, so mutating it does not affect this instance.
Returns number[] — [a, b, c, d]
Ipv4.is_unspecified()
net.Ipv4.is_unspecified() -> bool
Whether this is the 0.0.0.0 unspecified address.
Returns bool
Ipv4.is_loopback()
net.Ipv4.is_loopback() -> bool
Whether this address is in the 127.0.0.0/8 loopback range.
Returns bool
Ipv4.is_private()
net.Ipv4.is_private() -> bool
Whether this address is in one of the private-use ranges reserved by RFC
1918: 10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16.
Returns bool
Ipv4.is_link_local()
net.Ipv4.is_link_local() -> bool
Whether this address is in the 169.254.0.0/16 link-local range, used
for address autoconfiguration when no other address is available.
Returns bool
Ipv4.is_multicast()
net.Ipv4.is_multicast() -> bool
Whether this address is in the 224.0.0.0/4 multicast range.
Returns bool
Ipv4.is_broadcast()
net.Ipv4.is_broadcast() -> bool
Whether this is the 255.255.255.255 limited broadcast address.
Returns bool
Ipv4.is_documentation()
net.Ipv4.is_documentation() -> bool
Whether this address falls in one of the ranges reserved by RFC 5737 for
use in documentation and examples: 192.0.2.0/24, 198.51.100.0/24, or
203.0.113.0/24.
Returns bool
Ipv4.to_ipv6_mapped()
net.Ipv4.to_ipv6_mapped() -> Ipv6
Returns this address re-expressed as an IPv4-mapped IPv6 address, e.g.
192.168.1.10 becomes ::ffff:192.168.1.10.
Returns Ipv6
Ipv4.equals()
net.Ipv4.equals(other) -> bool
Whether other refers to the same address as this one.
Parameters
other(Ipv4) — The address to compare against
Returns bool
Ipv4.to_string()
net.Ipv4.to_string() -> string
This address in standard dotted-decimal notation, e.g. '192.168.1.10'.
Returns string
Ipv4.to_list()
net.Ipv4.to_list() -> list
Returns the octets of the Ipv4 address as a list
Returns list
Ipv6
class net.Ipv6
An IPv6 address, stored internally as eight 16-bit segments in network (i.e. big-endian, most significant segment first) order.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.Ipv6(a, b, c, d, e, f, g, h)
Returns a new instance of an Ipv6 from its eight segments,
given in the same left-to-right order they’d be written in colon-hex
notation, e.g. Ipv6(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1) for
2001:db8::1.
Parameters
a(number) — The 1st segment,0-65535b(number) — The 2nd segment,0-65535c(number) — The 3rd segment,0-65535d(number) — The 4th segment,0-65535e(number) — The 5th segment,0-65535f(number) — The 6th segment,0-65535g(number) — The 7th segment,0-65535h(number) — The 8th segment,0-65535
Raises Error if any segment is missing or outside 0-65535
Ipv6.unspecified()
net.Ipv6.unspecified() -> Ipv6
The :: unspecified address, used to mean “any local address” when
binding a socket.
Returns Ipv6
Ipv6.localhost()
net.Ipv6.localhost() -> Ipv6
The ::1 loopback address.
Returns Ipv6
Ipv6.parse()
net.Ipv6.parse(address) -> Ipv6
Parses an address given in standard colon-hex notation, including the
:: shorthand for a run of zero segments (at most one :: is allowed
per address) and the embedded dotted-decimal form used for
IPv4-mapped/-compatible addresses, e.g. '::ffff:192.168.1.10'.
Parameters
address(string) — The address to parse
Returns Ipv6
Raises Error if address is not a well-formed IPv6 address
Note: Zone identifiers (e.g. the
%eth0suffix used to disambiguate link-local addresses on a particular interface) are not supported and will cause parsing to fail.
Ipv6.segments()
net.Ipv6.segments() -> number[]
Returns the eight segments making up this address as a list, in the same left-to-right order they’d be written in colon-hex notation. Each call returns a fresh list, so mutating it does not affect this instance.
Returns number[] — [a, b, c, d, e, f, g, h]
Ipv6.is_unspecified()
net.Ipv6.is_unspecified() -> bool
Whether this is the :: unspecified address.
Returns bool
Ipv6.is_loopback()
net.Ipv6.is_loopback() -> bool
Whether this is the ::1 loopback address.
Returns bool
Ipv6.is_multicast()
net.Ipv6.is_multicast() -> bool
Whether this address is in the ff00::/8 multicast range.
Returns bool
Ipv6.is_unicast_link_local()
net.Ipv6.is_unicast_link_local() -> bool
Whether this address is in the fe80::/10 unicast link-local range.
Returns bool
Ipv6.to_ipv4_mapped()
net.Ipv6.to_ipv4_mapped() -> ?Ipv4
If this address is an IPv4-mapped address (::ffff:0:0/96), returns the
IPv4 address it maps to; otherwise returns nil.
Returns ?Ipv4
Ipv6.equals()
net.Ipv6.equals(other) -> bool
Whether other refers to the same address as this one.
Parameters
other(Ipv6) — The address to compare against
Returns bool
Ipv6.to_string()
net.Ipv6.to_string() -> string
This address in its canonical, most-compressed colon-hex notation - the
longest run of consecutive zero segments (if any run is at least two
segments long) is collapsed to ::; if more than one such run exists,
the leftmost is preferred.
Returns string
Ipv6.to_list()
net.Ipv6.to_list() -> list
Returns the segments of the Ipv6 address as a list
Returns list
Ip
class net.Ip
Either an Ipv4 or an Ipv6, for code that needs to
accept and work with addresses of either family without committing to
one upfront.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.Ip(inner)
Wraps an already-constructed Ipv4 or Ipv6.
Application code should generally prefer Ip.parse(), Ip.v4(), or
Ip.v6() over calling this directly.
Parameters
inner(Ipv4|Ipv6) — The address to wrap
Ip.v4()
net.Ip.v4(address) -> Ip
Parameters
address(Ipv4) — The address to wrap
Returns Ip
Ip.v6()
net.Ip.v6(address) -> Ip
Parameters
address(Ipv6) — The address to wrap
Returns Ip
Ip.parse()
net.Ip.parse(address) -> Ip
Parses address as either an IPv4 or IPv6 address, choosing the family
based on whether address contains a : character.
Parameters
address(string) — The address to parse
Returns Ip
Raises Error if address is not a well-formed IPv4 or IPv6
address
Ip.is_ipv4()
net.Ip.is_ipv4() -> bool
Whether this address is an Ipv4.
Returns bool
Ip.is_ipv6()
net.Ip.is_ipv6() -> bool
Whether this address is an Ipv6.
Returns bool
Ip.as_ipv4()
net.Ip.as_ipv4() -> ?Ipv4
Returns the wrapped Ipv4, or nil if this is an IPv6
address.
Returns ?Ipv4
Ip.as_ipv6()
net.Ip.as_ipv6() -> ?Ipv6
Returns the wrapped Ipv6, or nil if this is an IPv4
address.
Returns ?Ipv6
Ip.is_unspecified()
net.Ip.is_unspecified() -> bool
Whether this is the unspecified address for its family (0.0.0.0 or
::).
Returns bool
Ip.is_loopback()
net.Ip.is_loopback() -> bool
Whether this is the loopback address for its family (127.0.0.0/8 or
::1).
Returns bool
Ip.is_multicast()
net.Ip.is_multicast() -> bool
Whether this is a multicast address for its family.
Returns bool
Ip.equals()
net.Ip.equals(other) -> bool
Whether other refers to the same address as this one. Two addresses of
different families are never equal, even if one is the IPv4-mapped form
of the other - compare their canonical forms explicitly if that’s the
comparison you want.
Parameters
other(Ip) — The address to compare against
Returns bool
Ip.to_string()
net.Ip.to_string() -> string
This address in its family’s standard notation.
Returns string
Ip.to_list()
net.Ip.to_list() -> list
Returns the address in this IP as a list. If the address is an Ipv4 address, the list will contain four (4) elements. Otherwise, it will contain eight (8) elements.
Returns list
2026, Richard Ore and Zuri contributors
net.poll
import net
Everything here is re-exported by
net, soimport netis enough and the names are called asnet.*. Importingnet.pollon its own works too and reaches the same definitions.
Readiness polling: given a set of sockets, which of them can be read or written right now, without blocking.
Every other call in net blocks. TcpStream.read() waits until its own
socket has something to say and the thread does nothing else in the
meantime, which is right for a client and wrong for a server: a thread
that commits to one connection cannot serve another until that
connection lets it go. A server built only from blocking calls can hold
exactly as many concurrent connections as it has threads.
A Poller is the way out. It watches many sockets at once and tells you
which ones are ready, so a single thread can own hundreds of connections
and spend its time only on the ones with work:
import net
var server = net.TcpStream()
server.bind('127.0.0.1:8080')
var poller = net.Poller()
var clients = {}
var next_token = 1
# Token 0 watches the listener itself; an incoming connection makes
# it readable exactly the way data does on a connected socket.
poller.add(server, 0, net.READABLE)
while true {
for event in poller.wait(1000) {
var token = event[0]
if token == 0 {
var client = server.accept()
clients[next_token] = client
poller.add(client, next_token, net.READABLE)
next_token++
continue
}
var client = clients[token]
# A peer that has gone away is reported rather than left to be
# discovered by a read that returns nothing, forever.
if event[1] & (net.ERROR | net.HANGUP) != 0 {
poller.remove(token)
clients.remove(token)
client.close()
continue
}
echo client.read(1024).to_string()
}
}
Isolates
A Poller watches the sockets of the isolate that owns it, and nothing
about it crosses an isolate boundary. Several worker isolates each
keeping their own poller over their own connections is the intended
shape, and is exactly the shared-nothing model.
A socket that is closed, upgraded into a TlsStream, or moved to
another isolate stops being able to supply a descriptor. Rather than
this being a rule you have to remember, wait() reports such a socket
as ERROR against its own token, because the descriptor is resolved
fresh on every call and never remembered between them. It is not
possible for a poller to end up watching a descriptor that the operating
system has since handed to something else.
What polling does not do
Readiness is not a guarantee. A socket reported READABLE has data
now, but a read can still come up short, and a large write can still
be partial on a socket reported WRITABLE. Poll to decide what to work
on; keep handling short reads and writes the way you would anyway.
Constants
READABLE
net.READABLE: int = 1
Interest: tell me when this socket has data to read, a connection to accept, or an end-of-stream to observe.
WRITABLE
net.WRITABLE: int = 2
Interest: tell me when this socket can be written to without blocking.
ERROR
net.ERROR: int = 4
Readiness only: the socket has failed, or its handle can no longer supply a descriptor because it was closed, upgraded, or moved to another isolate. Deregister it.
HANGUP
net.HANGUP: int = 8
Readiness only: the peer has closed its end. Any data already sent is still readable, so drain the socket before closing it.
Classes
Poller
class net.Poller
A set of sockets to watch, each under a token of your choosing.
The token is whatever number is convenient for finding the socket again
-
an index into your own table, a connection id.
wait()hands back the tokens that are ready, never the sockets, so the mapping stays yours. -
printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.Poller()
Returns a new, empty Poller.
Poller.add()
net.Poller.add(socket, token: number, interest: ?number)
Starts watching socket under token.
Registering a token that is already present replaces it, so a caller that loses track cannot end up with one token naming two sockets.
Parameters
socket(TcpStream|TlsStream) — connected or bound; a bound listener becomes readable when a connection is waitingtoken(number) — your own identifier for this socketinterest(?number) —READABLE,WRITABLE, or both; defaults toREADABLE
Returns — Poller: this same instance, for chaining
Raises ValueError if interest names neither readability nor
writability
Poller.modify()
net.Poller.modify(token: number, interest: number)
Changes what token is watched for, leaving the socket alone.
This is how a connection with a reply still to write switches from waiting for a request to waiting for room to send, and back again.
Parameters
token(number)interest(number)
Returns — bool: whether the token was registered
Raises ValueError if interest names neither readability nor
writability
Poller.remove()
net.Poller.remove(token: number)
Stops watching token, and releases this poller’s reference to its
socket. Removing a token that is not registered does nothing rather than
failing.
Parameters
token(number)
Returns — bool: whether the token was registered
Poller.contains()
net.Poller.contains(token: number) -> bool
Whether token is currently registered.
Parameters
token(number)
Returns bool
Poller.interest()
net.Poller.interest(token: number) -> ?number
The interest currently registered for token, or nil.
Parameters
token(number)
Returns ?number
Poller.tokens()
net.Poller.tokens() -> list
Every registered token, in registration order.
Returns list
Poller.length()
net.Poller.length() -> number
How many sockets are being watched.
Returns number
Poller.is_empty()
net.Poller.is_empty() -> bool
Whether nothing is being watched.
Returns bool
Poller.clear()
net.Poller.clear()
Stops watching everything.
Returns — Poller: this same instance, for chaining
Poller.wait()
net.Poller.wait(timeout: ?number)
Waits until at least one registered socket is ready, and returns what happened.
Each entry is [token, flags], where flags is any combination of
READABLE, WRITABLE, ERROR and HANGUP. An empty list means the
timeout elapsed with nothing ready - and also that nothing is
registered, since waiting on an empty set could only ever sleep for the
full timeout to no purpose.
Parameters
timeout(?number) — milliseconds to wait;0polls without waiting, a negative number waits indefinitely. Defaults to waiting indefinitely.
Returns — list: [token, flags] pairs, one per ready socket
Raises Error on an underlying platform failure
Note: Test flags with
&, never==: a socket can be readable and hung up at the same time, which is what a peer that sent a final response and closed looks like.
Poller.to_string()
net.Poller.to_string()
Acceptor
class net.Acceptor
An accept loop that never parks the thread inside accept().
A blocking accept() waits inside the runtime, where nothing else on
the thread gets a turn: a signal trapped with os.on_signal() is not
delivered, and a flag telling the server to stop is not read, until a
connection happens to arrive. On an idle server that is never, which is
why Ctrl+C on one can appear to do nothing.
An Acceptor puts the listener into non-blocking mode and waits on a
Poller instead. next() hands over a connection when there is one,
and returns nil when the interval passes with nothing arriving, which
is the loop’s chance to look at whatever else it has to look at.
import net
import os
var listener = net.TcpStream()
listener.bind('127.0.0.1:8080')
var running = [true]
os.on_signal('INT', @() {
running[0] = false
return true
})
var acceptor = net.Acceptor(listener)
while running[0] {
var client = acceptor.next()
if client == nil {
continue
}
echo 'connection from ${client.peer_address()}'
client.close()
}
Nothing is added while connections are arriving: next() tries
accept() first, and only consults the poller when the queue is empty.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.Acceptor(listener, interval: ?number)
Takes over listener, which must already be bound.
The listener is put into non-blocking mode and left that way, so a
caller that also accepts from it directly gets nil when nothing is
waiting rather than a blocking wait.
Parameters
listener(TcpStream|UnixStream) — bound and listeninginterval(?number) — Millisecondsnext()waits before giving up on a round. Defaults to100. A connection that arrives wakes the wait at once, so this is the idle interval rather than added latency: it decides how soon a stopped server and a trapped signal are noticed.
Raises ValueError if interval is negative, which would wait
forever and defeat the point.
Raises Error if the listener is not bound, or cannot be put into
non-blocking mode.
Acceptor.next()
net.Acceptor.next() -> ?TcpStream|?UnixStream
The next connection, or nil when the interval passed with nothing
arriving.
nil means “go round again”, never “there will be no more”.
The connection comes back in blocking mode whatever mode the listener is in, since what usually follows is a request pipeline written against blocking reads bounded by timeouts.
Returns ?TcpStream|?UnixStream
Raises Error on an underlying accept failure, including the
listener having been closed.
Acceptor.wait_for_one()
net.Acceptor.wait_for_one() -> TcpStream|UnixStream
Waits for a connection however long it takes, the way a blocking
accept() would, while still coming up for air every interval.
Returns TcpStream|UnixStream
Raises Error on an underlying accept failure
Acceptor.listener()
net.Acceptor.listener() -> TcpStream|UnixStream
The listener this accepts from.
Returns TcpStream|UnixStream
Acceptor.to_string()
net.Acceptor.to_string()
2026, Richard Ore and Zuri contributors
net.resolver
import net
netexposes this asnet.resolver, soimport netis enough and the names are called asnet.resolver.*.import net.resolverreaches the same definitions directly.
A DNS resolver, written in Zuri from the wire format up.
net.resolve() answers the one question the operating system will
answer for you: what addresses does this name have. Everything else a
name can carry, from the mail servers of a domain to the text record a
service asks you to publish, needs a resolver that speaks DNS itself.
This is that resolver.
import net.resolver
for record in resolver.mx('example.com') {
echo '${record.preference} ${record.exchange}'
}
The module-level functions go through one shared Resolver built from
this machine’s own configuration, which is what most programs want.
Build your own when you need to ask a particular server, or ask it over
TLS:
import net.resolver { Resolver }
var dns = Resolver({
servers: ['1.1.1.1'],
tls: true,
server_name: 'cloudflare-dns.com',
})
echo dns.txt('example.com')
What it does
Queries go out over UDP and fall back to TCP when the answer does not fit, which is the behaviour every resolver is required to have and the reason a large TXT or DNSKEY record arrives intact. EDNS(0) is advertised so that fallback is rare. Each query carries a random identifier from a fresh ephemeral port, answers that do not match the question asked are discarded, and a server that does not answer in time gives way to the next one.
Answers are cached for as long as their own time-to-live says, and a name that does not exist is cached too, for as long as the zone’s own authority record allows. Caching is per resolver and can be turned off.
What it does not do
It does not validate DNSSEC. Signatures are carried through when a
server sends them, and the dnssec option asks for them, but nothing
here checks one. A program that needs a validated answer wants a
validating server and a trusted path to it, which is what tls gives.
Constants
TYPES
net.resolver.TYPES: dict = {...}
The record types this module knows how to read, by name. A type it has no name for is still parsed, reported by its number, and its data handed over as raw bytes.
CLASSES
net.resolver.CLASSES: dict = {...}
The classes a question can be asked in. IN, the internet class, is the
only one in general use and is the default everywhere here.
RCODES
net.resolver.RCODES: dict = {...}
What a server said about the question, by name. NOERROR means the
query succeeded, which is not the same as it having found anything.
Functions
parse_message()
net.resolver.parse_message(data) -> Response
Turns the bytes of a message into a Response.
Parameters
data(bytes)
Returns Response
Raises MalformedResponse if the bytes are not a well-formed
message
build_query()
net.resolver.build_query(id: number, name: string, type: number, klass: number, options: dict) -> bytes
Builds the bytes of a query.
Parameters
id(number) — The identifier to match the answer against.name(string) — The name to ask about.type(number)klass(number)options(dict) —recursion,edns,udp_sizeanddnssec.
Returns bytes
exchange()
net.resolver.exchange(server: string, query, options: dict) -> bytes
Asks one server one question and returns what it said.
Used by Resolver, and on its own when you want to talk to a particular
server without any of the retrying, caching or search list a resolver
puts around it.
Parameters
server(string) — An address, optionally with a port.query(bytes) — The message to send.options(dict) —timeout,port,tcp,tls,tls_configandserver_name.
Returns bytes — the answer, unparsed
Raises Error on a network failure or a timeout
system_servers()
net.resolver.system_servers() -> list
The nameservers this machine is configured to use, in the order the system lists them.
Read from /etc/resolv.conf on unix and from the network stack on
Windows. An address that is only meaningful together with an interface,
which is how a link-local IPv6 server is configured, is left out,
because nothing here can carry the interface along with it.
import net.resolver
echo resolver.system_servers()
# [127.0.0.53]
Returns list — of string, possibly empty
system_search()
net.resolver.system_search() -> list
The domain suffixes to try against a name that has no dots in it.
Usually empty away from a managed network. Resolver uses this as the
default for its search option.
Returns list — of string, possibly empty
reverse_name()
net.resolver.reverse_name(address: string) -> string
The name an address’s records are published under.
IPv4 addresses live under in-addr.arpa with their octets reversed, and
IPv6 addresses under ip6.arpa with one label per nibble.
import net.resolver
echo resolver.reverse_name('192.0.2.7')
echo resolver.reverse_name('2001:db8::1')
7.2.0.192.in-addr.arpa
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa
Parameters
address(string) — An IPv4 or IPv6 address.
Returns string
Raises Error if address is not an address.
shared()
net.resolver.shared() -> Resolver
The resolver the module-level functions use: one shared Resolver built
from this machine’s own configuration.
Reach for it when you want the shared cache but need a method the module does not re-export.
Returns Resolver
use()
net.resolver.use(instance: instance) -> Resolver
Replaces the shared resolver, so that every module-level function goes through the one given instead.
import net.resolver { Resolver }
import net.resolver
resolver.use(Resolver({ servers: ['9.9.9.9'] }))
Parameters
instance(Resolver)
Returns Resolver — the one now in use.
query()
net.resolver.query(name: string, type, klass) -> Response
Asks the shared resolver about one name and hands back the whole answer.
Parameters
name(string)type(string|number)klass(?string|number)
Returns Response
resolve()
net.resolver.resolve(name: string, type, klass) -> list
The records of one type published for a name, through the shared resolver.
Parameters
name(string)type(string|number)klass(?string|number)
Returns list — of Record
addresses()
net.resolver.addresses(name: string) -> list
Every address a name has, IPv4 first.
Parameters
name(string)
Returns list — of string
a()
net.resolver.a(name: string) -> list
The IPv4 addresses of a name.
Parameters
name(string)
Returns list — of string
aaaa()
net.resolver.aaaa(name: string) -> list
The IPv6 addresses of a name.
Parameters
name(string)
Returns list — of string
cname()
net.resolver.cname(name: string) -> string|nil
The name this one is an alias for, or nil.
Parameters
name(string)
Returns string|nil
mx()
net.resolver.mx(name: string) -> list
The mail servers for a domain, lowest preference first.
Parameters
name(string)
Returns list — of MailExchange
ns()
net.resolver.ns(name: string) -> list
The nameservers a domain is delegated to.
Parameters
name(string)
Returns list — of string
txt()
net.resolver.txt(name: string) -> list
The text records of a name.
Parameters
name(string)
Returns list — of string
soa()
net.resolver.soa(name: string) -> StartOfAuthority|nil
The authority record at the top of a name’s zone, or nil.
Parameters
name(string)
Returns StartOfAuthority|nil
srv()
net.resolver.srv(name: string) -> list
The hosts running a service, by priority and then by weight.
Parameters
name(string)
Returns list — of Service
ptr()
net.resolver.ptr(address: string) -> list
The names an address points back at.
Parameters
address(string)
Returns list — of string
caa()
net.resolver.caa(name: string) -> list
Which certificate authorities a domain allows to issue for it.
Parameters
name(string)
Returns list — of Certification
flush()
net.resolver.flush() -> Resolver
Empties the shared resolver’s cache.
Returns Resolver — the shared resolver.
Classes
ResolverError
class net.resolver.ResolverError < Error
The root of every error this module raises.
NameError
class net.resolver.NameError < ResolverError
The name does not exist. This is the NXDOMAIN answer, and it is an
answer rather than a failure: an authoritative server has said that
nothing is published under this name at all.
ServerError
class net.resolver.ServerError < ResolverError
The server answered, and the answer was a refusal or a failure of its
own. rcode carries the code it sent and rcode_name the name of that
code.
Constructor
net.resolver.ServerError(message: string, rcode: number)
Parameters
message(string)rcode(number) — The code the server replied with.
ResolverTimeout
class net.resolver.ResolverTimeout < ResolverError
No server answered in time. Every configured server was asked, as many
times as attempts allows, and none of them replied.
MalformedResponse
class net.resolver.MalformedResponse < ResolverError
Something came back that is not a DNS message, or is one that contradicts itself. A truncated answer, a compression pointer that loops, a record whose length runs past the end of the message.
NoServersError
class net.resolver.NoServersError < ResolverError
The resolver has nowhere to send a query: no servers were given and the system has none configured.
MailExchange
class net.resolver.MailExchange
One mail server for a domain, as an MX record names it.
Mail is delivered to the exchange with the lowest preference that
answers, so Resolver.mx() hands them back in that order.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.resolver.MailExchange(preference: number, exchange: string)
Parameters
preference(number) — Lower is tried first.exchange(string) — The name of the mail server.
MailExchange.to_string()
net.resolver.MailExchange.to_string()
Service
class net.resolver.Service
Where a service lives, as an SRV record names it: the host and port,
with the priority and weight that decide which of several to use.
Clients try the lowest priority first, and choose among equal priorities in proportion to their weight.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.resolver.Service(priority: number, weight: number, port: number, target: string)
Parameters
priority(number) — Lower is tried first.weight(number) — The share of traffic among equal priorities.port(number) — The port the service listens on.target(string) — The name of the host running it.
Service.address()
net.resolver.Service.address() -> string
The host:port this record points at, ready to hand to
TcpStream.connect().
Returns string
Service.to_string()
net.resolver.Service.to_string()
StartOfAuthority
class net.resolver.StartOfAuthority
The authority record at the top of a zone, which says who publishes it and how long the rest of the world may hold on to what it says.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.resolver.StartOfAuthority(primary, mailbox, serial, refresh, retry, expire, minimum)
Parameters
primary(string) — The name of the primary nameserver.mailbox(string) — The address of whoever is responsible, with the first dot standing in for the@.serial(number) — The version of the zone.refresh(number) — Seconds a secondary waits between checks.retry(number) — Seconds a secondary waits after a failed one.expire(number) — Seconds a secondary may serve the zone without reaching the primary.minimum(number) — Seconds a negative answer may be cached.
StartOfAuthority.to_string()
net.resolver.StartOfAuthority.to_string()
Certification
class net.resolver.Certification
A CAA record: which certificate authorities a domain allows to issue
for it.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.resolver.Certification(flags: number, tag: string, value: string)
Parameters
flags(number) — Bit 7 set means a client must understand the tag or refuse to issue.tag(string) —issue,issuewildoriodefin practice.value(string) — What the tag names, usually an authority’s domain.
Certification.to_string()
net.resolver.Certification.to_string()
Record
class net.resolver.Record
One resource record: a name, what kind of thing it holds, how long it may be cached, and the thing itself.
What data holds depends on the type:
| type | data |
|---|---|
A, AAAA | the address, as a string |
NS, CNAME, PTR, DNAME | the name, as a string |
MX | a MailExchange |
SRV | a Service |
SOA | a StartOfAuthority |
CAA | a Certification |
TXT | a list of strings, one per character-string |
| anything else | the record’s data, as bytes |
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.resolver.Record(name, type, klass, ttl, data)
Parameters
name(string) — The name this record is published under.type(number) — The record type, as a number.klass(number) — The record class, as a number.ttl(number) — Seconds this record may be cached for.data(any) — The record’s contents, per the table above.
Record.text()
net.resolver.Record.text() -> string
A TXT record’s strings joined into one, which is how a record split
across several character-strings is meant to be read. Any other type
raises.
Returns string
Raises ResolverError if this is not a TXT record.
Record.to_string()
net.resolver.Record.to_string() -> string
The record as a zone file would write it.
Returns string
Question
class net.resolver.Question
The question a message asks, echoed back in the answer so a reply can be matched to what it replies to.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.resolver.Question(name: string, type: number, klass: number)
Parameters
name(string)type(number)klass(number)
Question.to_string()
net.resolver.Question.to_string()
Response
class net.resolver.Response
A whole answer: what was asked, what came back, and what the server said about it.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.resolver.Response(id, flags, question, answers, authorities, additionals, extended)
Response.records_of()
net.resolver.Response.records_of(type) -> list
The answers of one type, leaving out the aliases and anything else the server sent along.
Parameters
type(string|number)
Returns list — of Record
Response.ok()
net.resolver.Response.ok() -> Response
Raises when the server reported anything other than success. A name that
does not exist raises NameError; everything else raises ServerError.
Returns Response — itself, so this can be chained.
Raises NameError|ServerError
Response.to_string()
net.resolver.Response.to_string()
Resolver
class net.resolver.Resolver
Asks questions of the domain name system and reads the answers.
One resolver holds its own configuration and its own cache, so a program can keep several: one for the machine’s own servers, one for a particular provider over TLS, one with caching off for a test.
import net.resolver { Resolver }
var dns = Resolver()
echo dns.addresses('example.com')
echo dns.mx('example.com')
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.resolver.Resolver(options: ?dict)
Builds a resolver.
Every option has a default, and the defaults describe this machine, so
Resolver() behaves the way the rest of the system does.
| option | default | what it does |
|---|---|---|
servers | the system’s own | the addresses to ask, in order |
port | 53, or 853 with tls | the port to ask them on |
timeout | 5000 | milliseconds one server gets to answer |
attempts | 2 | times to go round the whole list |
tcp | false | send over TCP rather than UDP |
tls | false | send over TLS, which implies TCP |
server_name | none | the name to verify the TLS certificate against |
tls_config | a default TlsConfig | the trust settings for TLS |
search | the system’s own | suffixes to try against a short name |
ndots | 1 | dots a name needs before it is tried unsuffixed first |
recursion | true | ask the server to do the work |
edns | true | advertise a larger UDP payload |
udp_size | 1232 | how much larger |
dnssec | false | ask for signature records, without validating them |
cache | true | remember answers for as long as they say |
cache_size | 512 | entries to keep before evicting |
Parameters
options(?dict)
Raises ResolverError if an option is not one of the above.
Resolver.servers()
net.resolver.Resolver.servers() -> list
The servers this resolver asks, in the order it asks them.
Returns list — of string
Raises NoServersError if none were given and the system has none.
Resolver.search()
net.resolver.Resolver.search() -> list
The suffixes this resolver tries against a name too short to be meant on its own.
Returns list — of string, possibly empty
Resolver.flush()
net.resolver.Resolver.flush() -> Resolver
Empties this resolver’s cache.
Returns Resolver — itself.
Resolver.query()
net.resolver.Resolver.query(name: string, type, klass) -> Response
Asks about one name and hands back the whole answer, including the records the server sent along with it.
The search list is applied, so a short name is tried with each
configured suffix. The answer is not checked: a name that does not exist
comes back as a Response whose rcode says so, which is what ok()
is for.
import net.resolver { Resolver }
var response = Resolver().query('example.com', 'MX').ok()
echo response.answers
Parameters
name(string)type(string|number) — A name fromTYPESor a number.klass(?string|number) —INunless told otherwise.
Returns Response
Raises NoServersError if there is nowhere to send the query.
Raises ResolverTimeout if no server answered.
Raises MalformedResponse if what came back is not a valid answer.
Resolver.resolve()
net.resolver.Resolver.resolve(name: string, type, klass) -> list
The records of one type published for a name.
Aliases are followed: asking for the A records of a name that is a
CNAME gives the addresses at the end of the chain. A name that exists
but publishes nothing of this type gives an empty list, which is a
different thing from the name not existing.
Parameters
name(string)type(string|number)klass(?string|number)
Returns list — of Record
Raises NameError if the name does not exist.
Raises ServerError if the server reported a failure.
Resolver.addresses()
net.resolver.Resolver.addresses(name: string) -> list
Every address a name has, IPv4 first and then IPv6.
Parameters
name(string)
Returns list — of string
Raises NameError if the name does not exist.
Resolver.a()
net.resolver.Resolver.a(name: string) -> list
The IPv4 addresses of a name.
Parameters
name(string)
Returns list — of string
Raises NameError if the name does not exist.
Resolver.aaaa()
net.resolver.Resolver.aaaa(name: string) -> list
The IPv6 addresses of a name.
Parameters
name(string)
Returns list — of string
Raises NameError if the name does not exist.
Resolver.cname()
net.resolver.Resolver.cname(name: string) -> string|nil
The name this one is an alias for, or nil when it is not one.
Parameters
name(string)
Returns string|nil
Raises NameError if the name does not exist.
Resolver.mx()
net.resolver.Resolver.mx(name: string) -> list
The mail servers for a domain, lowest preference first, which is the order they should be tried in.
import net.resolver { Resolver }
for server in Resolver().mx('example.com') {
echo '${server.preference} ${server.exchange}'
}
Parameters
name(string)
Returns list — of MailExchange
Raises NameError if the name does not exist.
Resolver.ns()
net.resolver.Resolver.ns(name: string) -> list
The nameservers a domain is delegated to.
Parameters
name(string)
Returns list — of string
Raises NameError if the name does not exist.
Resolver.txt()
net.resolver.Resolver.txt(name: string) -> list
The text records of a name, one string per record.
A record split across several character-strings, which is how anything longer than 255 bytes is published, is joined back together, because the split is a limit of the wire format and not part of what was published.
Parameters
name(string)
Returns list — of string
Raises NameError if the name does not exist.
Resolver.soa()
net.resolver.Resolver.soa(name: string) -> StartOfAuthority|nil
The authority record at the top of the zone a name belongs to, or nil
when the name is not the top of one.
Parameters
name(string)
Returns StartOfAuthority|nil
Raises NameError if the name does not exist.
Resolver.srv()
net.resolver.Resolver.srv(name: string) -> list
The hosts running a service, by priority and then by weight.
Among equal priorities the record with the larger weight should take proportionally more traffic; this returns them heaviest first, and choosing between them is the caller’s to make.
Parameters
name(string) — The full service name, such as_imap._tcp.example.com.
Returns list — of Service
Raises NameError if the name does not exist.
Resolver.ptr()
net.resolver.Resolver.ptr(address: string) -> list
The names an address points back at.
Takes the address itself rather than a reversed name, so there is no
need to build 4.3.2.1.in-addr.arpa by hand.
import net.resolver { Resolver }
echo Resolver().ptr('8.8.8.8')
# [dns.google]
Parameters
address(string) — An IPv4 or IPv6 address.
Returns list — of string
Raises NameError if nothing is published for the address.
Resolver.caa()
net.resolver.Resolver.caa(name: string) -> list
Which certificate authorities a domain allows to issue for it.
Parameters
name(string)
Returns list — of Certification
Raises NameError if the name does not exist.
Resolver.to_string()
net.resolver.Resolver.to_string()
2026, Richard Ore and Zuri contributors
net.tcp
import net
Everything here is re-exported by
net, soimport netis enough and the names are called asnet.*. Importingnet.tcpon its own works too and reaches the same definitions.
This module provides a complete implementation of the Transmission Control Protocol as specified in IETF RFC 793. It provides an interface for working with connected socket and listening socket alike.
It is meant to be used to provide more controlled and specific TCP-based operating system features and for implementing various standard and custom network protocols and specifications.
The example below shows how to use this module to create a basic HTTP client.
import net
var stream = net.TcpStream()
stream.connect('example.com:80')
stream.write_all('GET / HTTP/1.0\r\nHost: example.com\r\n\r\n')
echo stream.read_as_string()
stream.close()
Like earlier said, this module provides functionally for both connected sockets and listening sockets. A simple demonstration of a listening socket is given below which implements a basic and foundational HTTP server that recieves requests on port 8080 and returns a message containig the address of the client to the said client.
import net
var server = net.TcpStream()
server.bind('0.0.0.0:8080')
echo 'listening on ${server.local_address()}'
while true {
var client = server.accept()
client.write_all('hello, ${client.peer_address()}\n')
client.close()
}
Functions
resolve()
net.resolve(address)
Resolves address - a host:port string whose host may be a name or an
IP literal - into every socket address it names, in the order the
resolver returned them.
A hostname routinely maps to several addresses (an IPv6 and an IPv4 one
at the very least), which is why this returns a list rather than a
single address. connect() does its own resolution when handed a name,
so this is mainly useful when you need the concrete address first: to
connect with a timeout (which requires a literal), to prefer one address
family, or simply to look up what a name points at.
import net
echo net.resolve('example.com:443')
# [93.184.215.14:443]
Parameters
address(string|SocketAddr) — Thehost:portto resolve
Returns — list: one or more SocketAddr
Raises Error if the name cannot be resolved, or resolves to
nothing
Classes
Shutdown
class net.Shutdown
Used with TcpStream.shutdown() to select which half (or both halves)
of a full-duplex TCP connection to shut down. Shutting down a stream
only affects the local socket as it is a local operation and does not
send anything resembling a FIN/EOF marker to the peer beyond what the
underlying platform’s shutdown(2) implementation does.
TcpStream
class net.TcpStream
A TCP socket, in either its connected-stream or listening-server role.
- printable — has a
@to_string(), soechoandprint()show something useful
Note: On Unix systems, writes to the underlying socket in
SOCK_STREAMmode are made withMSG_NOSIGNALflag. This suppresses the emission of theSIGPIPEsignal when writing to disconnected socket.
Note: It’s a good practice to call
close()explicitly once you are done with an instance instead of relying on Zuri to release the socket for you.
Constructor
net.TcpStream(_ptr)
Returns a new instance of a TcpStream.
TcpStream.native_ptr()
net.TcpStream.native_ptr() -> Ptr
Hands out this stream’s underlying native socket handle. _ptr is
private the way every field prefixed with _ is, so a sibling module
like tls - which needs to take an already-connected TcpStream and
wrap it, without belonging to this class or inheriting from it - has no
other way to reach it.
Returns Ptr
TcpStream.connect()
net.TcpStream.connect(address, timeout)
Opens a TCP connection to a remote host, turning this instance into a connected stream.
address may be a hostname or an IP literal, combined with a port, e.g.
'example.com:80' or '127.0.0.1:8080'. A hostname may resolve to
several addresses; they are tried in turn and the first one that
succeeds is used, meaning this call can succeed even though some of the
resolved addresses were unreachable.
Pass timeout to bound how long the connection attempt may take before
giving up. When a timeout is given, address must already be a concrete
ip:port literal rather than a hostname, as no DNS resolution would be
performed and no multi-address fallback is guaranteed.
Parameters
address(string|SocketAddr) — The host:port to connect totimeout(?int) — Optional milliseconds to wait before giving up; omit for an untimed connection attempt
Raises Error if resolution fails, if every resolved address
refuses the connection, or (when timeout is given) if address cannot
be parsed as an ip:port literal or the timeout elapses first.
Note: a timeout of
0is the same as specifying no timeout.
TcpStream.bind()
net.TcpStream.bind(address)
Binds this instance to address and starts listening for incoming
connections, turning it into a listening server.
address may be a hostname or an IP literal combined with a port, e.g.
'0.0.0.0:8080' or '[::]:8080'; port 0 asks the OS to assign an
available ephemeral port, which can then be discovered via
local_address(). If address resolves to multiple addresses, binding
is attempted against each in turn until one succeeds.
Parameters
address(string|SocketAddr) — The host:port to bind and listen on
Raises Error if resolution fails or every resolved address fails
to bind (e.g. because it is already in use)
TcpStream.accept()
net.TcpStream.accept() -> ?TcpStream
Accepts a new incoming connection. Only valid once this instance has
been bind()-ed.
Blocks until a connection arrives, and returns a fully connected
TcpStream for the new client. To find out who connected, call
peer_address() on it.
In non-blocking mode (see set_non_blocking()) it never blocks: it
returns nil when no connection is waiting. That is an answer, not a
failure, so nothing is raised for it.
The accepted stream can carry its own blocking mode, which is a
platform’s default rather than this listener’s. Set it explicitly with
set_non_blocking() on the returned stream if it matters.
Returns ?TcpStream — a stream connected to the newly accepted
client, or nil in non-blocking mode with nobody waiting
Raises Error on an underlying I/O error, or if this instance has
not been bound
TcpStream.peer_address()
net.TcpStream.peer_address() -> SocketAddr
Returns the socket address of the remote peer this stream is connected to.
Returns SocketAddr — the peer’s address
Raises Error if this instance is not a connected stream
TcpStream.local_address()
net.TcpStream.local_address() -> SocketAddr
Returns the local socket address of this instance, i.e. the address it connected from if it is a stream, or the address it is listening on if it is a listener.
This is particularly useful for discovering the actual port chosen by
the OS after binding to port 0.
Returns SocketAddr — the local address
Raises Error if this instance is neither connected nor bound
TcpStream.shutdown()
net.TcpStream.shutdown(how)
Shuts down the read, write, or both halves of this connection. See
Shutdown for the meaning of each value; defaults to Shutdown.BOTH
when how is omitted. Only valid on a connected stream. Calling this
more than once may return an error on some platforms.
Parameters
how(?int) — One of theShutdownconstants
Raises Error if the underlying shutdown(2) call fails, or if
this instance is not a connected stream
TcpStream.set_read_timeout()
net.TcpStream.set_read_timeout(timeout)
Sets the timeout for future read()/read_exact()/read_all()/
read_as_string() calls.
Pass a number of milliseconds greater than 0 to bound how long a read
may block before raising a timeout error, or pass 0 (or any negative
number) to remove the timeout entirely and block indefinitely.
Parameters
timeout(number) — Milliseconds, or a negative number to disable the timeout
Raises Error if the platform rejects the value
Note: A set timeout is an upper bound: a future read may still return before that bound is reached, and system timers mean the actual time elapsed before a timeout error can be slightly longer than what was requested.
TcpStream.get_read_timeout()
net.TcpStream.get_read_timeout() -> number
Returns the currently configured read timeout in milliseconds, or -1
if no timeout is set (i.e. reads block indefinitely).
Returns number — milliseconds, or -1 if unset
Note: Some platforms do not provide access to the current timeout.
TcpStream.set_write_timeout()
net.TcpStream.set_write_timeout(timeout)
Sets the timeout for future write()/write_all() calls. Same rules as
set_read_timeout(): a number of milliseconds greater than 0 bounds
how long a write may block, 0 (or any negative number) disables the
timeout.
Parameters
timeout(number) — Milliseconds, or a negative number to disable the timeout
Raises Error if the platform rejects the value
TcpStream.get_write_timeout()
net.TcpStream.get_write_timeout() -> number
Returns the currently configured write timeout in milliseconds, or -1
if no timeout is set (i.e. writes block indefinitely).
Returns number — milliseconds, or -1 if unset
Note: Some platforms do not provide access to the current timeout.
TcpStream.peek()
net.TcpStream.peek(length) -> bytes
Peeks at up to length bytes of incoming data without consuming it such
that a subsequent read()/read_exact() call will still see the peeked
bytes. Defaults to peeking a single byte when length is omitted.
Successful peeks can, and often do, return fewer bytes than length.
Parameters
length(number) — The maximum number of bytes to peek
Returns bytes — up to length bytes currently in the receive
buffer (nil if none are available yet)
Raises Error on an underlying I/O error
TcpStream.set_nodelay()
net.TcpStream.set_nodelay(nodelay)
Enables or disables TCP_NODELAY (i.e. disables or enables Nagle’s
algorithm). When nodelay is enabled, small writes are sent immediately
instead of being buffered in the hope of coalescing them with subsequent
writes, trading throughput for lower latency.
Parameters
nodelay(bool) —trueto disable Nagle’s algorithm
Raises Error on an underlying I/O error
TcpStream.get_nodelay()
net.TcpStream.get_nodelay() -> bool
Returns whether TCP_NODELAY is currently enabled on this stream.
Returns bool
TcpStream.set_ttl()
net.TcpStream.set_ttl(ttl)
Sets IP_TTL (the IPv4 time-to-live / IPv6 hop limit) for packets sent
on this socket. For a listener, this is the TTL that will be used by
sockets it accepts.
Parameters
ttl(number) — The time-to-live value,0-255
Raises Error on an underlying I/O error
TcpStream.get_ttl()
net.TcpStream.get_ttl() -> number
Returns the currently configured IP_TTL value for this socket.
Returns number
Raises Error on an underlying I/O error
TcpStream.get_error()
net.TcpStream.get_error() -> ?string
Retrieves and clears the value of the socket’s SO_ERROR option - i.e.
any pending error the OS has recorded for this socket without yet
surfacing through a failed read, write, or accept().
Returns ?string — a description of the pending error, or nil if
there is none
Raises Error on an underlying I/O error while querying the socket
TcpStream.set_non_blocking()
net.TcpStream.set_non_blocking(nonblocking)
Puts the socket into or out of non-blocking mode. In non-blocking mode,
read()/write()/accept() and friends return immediately with an
error instead of blocking when the operation would otherwise wait on
I/O.
Parameters
nonblocking(bool) —trueto enable non-blocking mode
Raises Error on an underlying I/O error
TcpStream.read()
net.TcpStream.read(length) -> bytes
Reads up to length bytes from the stream into a newly allocated byte
buffer, without necessarily filling it and returns the buffer. It is
entirely normal for this to return fewer bytes than length; even when
more data will eventually be available. So, callers that need an exact
amount of data should call this in a loop or use read_exact() instead.
An empty result means the peer has closed its write half (EOF).
Parameters
length(number) — The maximum number of bytes to read
Returns bytes — up to length bytes read from the stream (may be
empty on EOF)
Raises Error on an underlying I/O error, or if the configured read
timeout elapses first
TcpStream.read_exact()
net.TcpStream.read_exact(length) -> bytes
Reads exactly length bytes from the stream, blocking (subject to the
read timeout) until that many bytes have arrived.
If the stream reaches EOF before length bytes have been read, this
raises an error; the contents of any bytes already read are lost.
Parameters
length(number) — The exact number of bytes to read
Returns bytes — exactly length bytes
Raises Error if EOF is reached early, or on any other underlying
I/O error
TcpStream.read_all()
net.TcpStream.read_all() -> bytes
Reads from the stream until EOF, returning everything received as a
single byte stream. Since a stream is only closed by the peer (or by
calling shutdown()/close()), this will block until the connection is
closed. Therefore, do not call this on a stream you expect to stay open,
or set a read timeout first.
Returns bytes — every byte read up to EOF
Raises Error on an underlying I/O error
TcpStream.read_as_string()
net.TcpStream.read_as_string() -> string
Reads from the stream until EOF, decoding everything received as UTF-8
text. Like read_all(), this blocks until the peer closes the
connection. Fails if the received bytes are not valid UTF-8, in which
case the data already read is discarded.
Returns string — every byte read up to EOF, decoded as UTF-8
Raises Error on an underlying I/O error, or if the data is not
valid UTF-8
TcpStream.write()
net.TcpStream.write(data) -> number
Writes data to the stream, returning the number of bytes actually
written. A successful call may write fewer bytes than data contains.
Use write_all() if you need every byte written before continuing.
Parameters
data(bytes|string) — The bytes (or UTF-8 text) to write
Returns number — the number of bytes actually written
Raises Error on an underlying I/O error, or if the configured
write timeout elapses first
TcpStream.write_all()
net.TcpStream.write_all(data)
Writes the entirety of data to the stream, blocking (subject to the
write timeout) and retrying internally until every byte has been
written.
Parameters
data(bytes|string) — The bytes (or UTF-8 text) to write
Raises Error on an underlying I/O error, or if the configured
write timeout elapses first
TcpStream.flush()
net.TcpStream.flush()
Flushes any buffered output. TCP streams are unbuffered at this layer,
so this is a no-op and doesn’t currently do anything for TcpStream.
This is a no-op provided purely so this class satisfies the same
interface as other writable streams in the standard library.
Raises Error on an underlying I/O error
TcpStream.is_connected()
net.TcpStream.is_connected() -> bool
Whether this instance currently wraps a connected stream (i.e.
connect() has succeeded and close() has not been called since).
Returns bool
TcpStream.is_bound()
net.TcpStream.is_bound() -> bool
Whether this instance currently wraps a bound listener (i.e. bind()
has succeeded and close() has not been called since).
Returns bool
TcpStream.close()
net.TcpStream.close()
Flushes, shuts down, and releases the underlying socket, whether it is a connected stream or a bound listener. It is advised to call this explicitly once you are done with the socket rather than relying on the Zuri garbage collector to do it for you eventually. Safe to call more than once, and safe to call on an instance that was never connected or bound.
TcpStream.to_string()
net.TcpStream.to_string() -> string
A human readable representation of this socket, showing whichever of the connected-stream or bound-listener addresses apply.
Returns string
2026, Richard Ore and Zuri contributors
net.tls
import net
netexposes this asnet.tls, soimport netis enough and the names are called asnet.tls.*.import net.tlsreaches the same definitions directly.
Transport Layer Security over a TcpStream, built on top of rustls.
TlsStream.connect()/accept() take an already-connected TcpStream
and run a handshake over it, turning it into an encrypted,
certificate-verified stream. That works equally well right after
connect() (HTTPS, IMAPS) or after a STARTTLS-style plaintext exchange
(SMTP, FTPS) - see the second example below.
A basic HTTPS-style client:
import net
import net.tls
var tcp = net.TcpStream()
tcp.connect('example.com:443')
var config = tls.TlsConfig()
var stream = tls.TlsStream.connect(tcp, config, 'example.com')
stream.write_all('GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n')
echo stream.read_as_string()
stream.close()
A STARTTLS-style upgrade looks almost identical, except the wrap happens after a round of plaintext instead of right after connect:
import net
import net.tls
var tcp = net.TcpStream()
tcp.connect('mail.example.com:587')
echo tcp.read_as_string() # server greeting
tcp.write_all('STARTTLS\r\n')
echo tcp.read_as_string() # server's go-ahead
var stream = tls.TlsStream.connect(tcp, tls.TlsConfig(), 'mail.example.com')
A minimal server, reusing TcpStream’s own bind/accept for the
plaintext side of things:
import net
import net.tls
var config = tls.TlsConfig()
config.set_cert_chain(cert_chain_pem, private_key_pem)
var server = net.TcpStream()
server.bind('0.0.0.0:8443')
while true {
var client = server.accept()
var tls_client = tls.TlsStream.accept(client, config)
tls_client.write_all('hello over TLS\n')
tls_client.close()
}
Classes
TlsConfig
class net.tls.TlsConfig
The handful of choices a TLS handshake needs: which certificate authorities to trust, an optional certificate and private key to present (as a server, or as a client doing mutual TLS), and which protocol versions and ALPN protocols are acceptable.
A single TlsConfig can be reused for many connections - build it once
and pass it to every connect()/accept() call rather than
constructing a fresh one per connection.
Constructor
net.tls.TlsConfig()
Returns a new TlsConfig with sensible defaults: the bundled Mozilla
root certificate list, no client/server certificate, no ALPN protocols,
and both TLS 1.2 and TLS 1.3 enabled.
TlsConfig.native_ptr()
net.tls.TlsConfig.native_ptr() -> Ptr
Hands out this config’s underlying native handle, the same way
TcpStream.native_ptr() does - TlsStream’s connect()/accept()
need it and, like any _-prefixed field, _ptr isn’t otherwise
reachable from outside this class.
Returns Ptr
TlsConfig.set_root_store()
net.tls.TlsConfig.set_root_store(mode)
Chooses where trusted root certificate authorities come from when verifying a peer’s certificate chain.
'bundled' (the default) uses a compiled-in copy of Mozilla’s root
list, which is portable and doesn’t depend on anything being installed
on the machine. 'native' uses the operating system’s own trust store
instead, which picks up locally-installed or enterprise-managed CAs that
the bundled list doesn’t know about.
Parameters
mode(string) —'bundled'or'native'
Raises Error if mode is neither, or (for 'native') if the OS
trust store can’t be read
TlsConfig.add_ca_pem()
net.tls.TlsConfig.add_ca_pem(pem)
Adds one or more PEM-encoded certificates to this config’s trusted
roots, on top of whatever set_root_store() already selected. Call this
more than once to add several custom CAs
- useful for trusting a self-signed development certificate or a private/internal CA alongside the normal public trust store.
Parameters
pem(string) — One or more PEM-encoded CA certificates
Raises Error if none of the certificates in pem could be parsed
TlsConfig.set_cert_chain()
net.tls.TlsConfig.set_cert_chain(cert_chain_pem, key_pem)
Sets the certificate chain and private key this side of the connection
presents to the other. Required before TlsStream.accept() can be used
with this config (a server always needs a certificate); optional for
TlsStream.connect(), where it’s only needed if the server requires a
client certificate (mutual TLS).
Parameters
cert_chain_pem(string) — The end-entity certificate followed by any intermediate certificates, all PEM-encodedkey_pem(string) — The PEM-encoded private key matching the end-entity certificate
Raises Error if either PEM can’t be parsed, or if the key doesn’t
match the certificate
TlsConfig.require_client_cert()
net.tls.TlsConfig.require_client_cert(required)
For a server config: whether to require the connecting client to present
a trusted certificate (mutual TLS). Verified against the same root store
set_root_store()/add_ca_pem() configured. Has no effect on a config
only ever used with TlsStream.connect().
Parameters
required(bool)
TlsConfig.set_alpn()
net.tls.TlsConfig.set_alpn(protocols)
Sets the list of ALPN (Application-Layer Protocol Negotiation) protocol
names this side is willing to speak, in preference order, e.g. ['h2', 'http/1.1'].
The negotiated protocol - if any - is available afterwards via
TlsStream.alpn_protocol().
Parameters
protocols(list) — Protocol names, most preferred first
TlsConfig.set_versions()
net.tls.TlsConfig.set_versions(min, max)
Restricts which TLS protocol versions are acceptable. Pass '1.2' or
'1.3' for either bound, or nil to leave that bound open. Both
versions are enabled by default.
Parameters
min(?string) — The lowest acceptable version, ornilmax(?string) — The highest acceptable version, ornil
Raises Error if min or max is neither '1.2', '1.3', nor
nil
TlsConfig.set_insecure()
net.tls.TlsConfig.set_insecure(insecure)
Disables certificate verification entirely when insecure is true -
no chain-of-trust check, no hostname check, no expiry check. Handshake
signatures are still verified cryptographically, so this isn’t a total
bypass, just an untrusted one.
Parameters
insecure(bool)
Note: This exists for local development and testing against self-signed certificates. Never enable it for a connection that handles real traffic.
PeerCertificate
class net.tls.PeerCertificate
The subset of a peer’s certificate that’s useful to inspect from Zuri
code, without needing a full ASN.1/X.509 library on hand. Returned by
TlsStream.peer_certificate().
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.tls.PeerCertificate(der, subject, issuer, sans, not_before, not_after)
PeerCertificate.der()
net.tls.PeerCertificate.der() -> bytes
The raw DER-encoded certificate, exactly as the peer presented it.
Returns bytes
PeerCertificate.subject()
net.tls.PeerCertificate.subject() -> string
The certificate’s subject, as an RFC 4514-style distinguished name
string, e.g. 'CN=example.com,O=Example Inc,C=US'.
Returns string
PeerCertificate.issuer()
net.tls.PeerCertificate.issuer() -> string
The certificate’s issuer, in the same distinguished-name format as
subject().
Returns string
PeerCertificate.sans()
net.tls.PeerCertificate.sans() -> list
The certificate’s Subject Alternative Names - the DNS names, IP addresses, email addresses, and URIs it’s actually valid for.
Returns list — a list of strings, possibly empty
PeerCertificate.not_before()
net.tls.PeerCertificate.not_before() -> number
The start of the certificate’s validity period, as seconds since the Unix epoch (UTC).
Returns number
PeerCertificate.not_after()
net.tls.PeerCertificate.not_after() -> number
The end of the certificate’s validity period, as seconds since the Unix epoch (UTC).
Returns number
PeerCertificate.to_string()
net.tls.PeerCertificate.to_string()
TlsStream
class net.tls.TlsStream
An established TLS connection, wrapping an already-connected
TcpStream. Always created by wrapping an existing stream - via
connect() on the client side or accept() on the server side - never
constructed directly.
- printable — has a
@to_string(), soechoandprint()show something useful
Note: The wrapped
TcpStreamis consumed: onceconnect()/accept()returns, the originalTcpStreaminstance is dead (the same wayTcpStream.close()leaves it dead) and every further read/write happens through the returnedTlsStreaminstead.
Constructor
net.tls.TlsStream(_ptr)
TlsStream.connect()
net.tls.TlsStream.connect(tcp_stream, config, server_name) -> TlsStream
Performs a client-side TLS handshake over tcp_stream, verifying the
peer’s certificate against config’s trusted roots and against
server_name.
tcp_stream must already be connected - to the target host for an
implicit-TLS protocol like HTTPS, or to a plaintext connection that’s
already had some protocol-specific greeting exchanged over it for a
STARTTLS-style protocol.
Parameters
tcp_stream(TcpStream) — An already-connected stream; consumed by this callconfig(TlsConfig) — The trust roots and options to handshake withserver_name(string) — The hostname to verify the peer’s certificate against (also sent as the SNI extension)
Returns TlsStream
Raises Error if the handshake fails, or the peer’s certificate
isn’t trusted or doesn’t match server_name
TlsStream.accept()
net.tls.TlsStream.accept(tcp_stream, config) -> TlsStream
Performs a server-side TLS handshake over tcp_stream, presenting
config’s certificate chain to the connecting peer.
tcp_stream must already be connected - typically the result of
TcpStream.accept(), either wrapped immediately (implicit TLS) or after
exchanging a protocol-specific plaintext greeting first
(STARTTLS-style).
Parameters
tcp_stream(TcpStream) — An already-accepted stream; consumed by this callconfig(TlsConfig) — Must have a certificate chain set viaTlsConfig.set_cert_chain()
Returns TlsStream
Raises Error if the handshake fails, config has no certificate
chain, or (with require_client_cert()) the client didn’t present a
trusted certificate
TlsStream.native_ptr()
net.tls.TlsStream.native_ptr() -> Ptr
Hands out this stream’s underlying native handle, the same way
TcpStream.native_ptr() does. net.Poller needs it in order to watch
the socket underneath the TLS session, and _ptr is not otherwise
reachable from outside this class.
Returns Ptr
TlsStream.read()
net.tls.TlsStream.read(length) -> bytes
Reads up to length bytes of decrypted application data. Same
blocking/timeout/EOF semantics as TcpStream.read().
Parameters
length(number) — The maximum number of bytes to read
Returns bytes — up to length bytes (may be empty on EOF)
Raises Error on an underlying I/O or TLS record error
TlsStream.read_exact()
net.tls.TlsStream.read_exact(length) -> bytes
Reads exactly length bytes, blocking until that many bytes have
arrived.
Parameters
length(number) — The exact number of bytes to read
Returns bytes — exactly length bytes
Raises Error if EOF is reached early, or on any other I/O or TLS
record error
TlsStream.read_all()
net.tls.TlsStream.read_all() -> bytes
Reads until EOF (i.e. until the peer sends close_notify and closes the
connection), returning everything received.
Returns bytes — every byte read up to EOF
Raises Error on an underlying I/O or TLS record error
TlsStream.read_as_string()
net.tls.TlsStream.read_as_string() -> string
Reads until EOF, decoding everything received as UTF-8 text.
Returns string — every byte read up to EOF, decoded as UTF-8
Raises Error on an underlying I/O error, a TLS record error, or if
the data isn’t valid UTF-8
TlsStream.write()
net.tls.TlsStream.write(data) -> number
Encrypts and writes data, returning the number of plaintext bytes
actually written. A successful call may write fewer bytes than data
contains; use write_all() if every byte needs to go out before
continuing.
Parameters
data(bytes|string) — The bytes (or UTF-8 text) to write
Returns number — the number of bytes actually written
Raises Error on an underlying I/O or TLS record error
TlsStream.write_all()
net.tls.TlsStream.write_all(data)
Encrypts and writes the entirety of data, blocking and retrying
internally until every byte has been written.
Parameters
data(bytes|string) — The bytes (or UTF-8 text) to write
Raises Error on an underlying I/O or TLS record error
TlsStream.flush()
net.tls.TlsStream.flush()
Flushes any buffered ciphertext out to the underlying socket.
Raises Error on an underlying I/O error
TlsStream.shutdown()
net.tls.TlsStream.shutdown()
Sends a close_notify alert, telling the peer this side is done
writing. Unlike close(), the stream stays usable afterwards for
reading whatever the peer still has left to send.
Raises Error on an underlying I/O error
TlsStream.close()
net.tls.TlsStream.close()
Sends close_notify (best-effort - a peer that’s already gone won’t
stop this from succeeding) and releases the underlying socket. Safe to
call more than once.
TlsStream.alpn_protocol()
net.tls.TlsStream.alpn_protocol() -> ?string
The ALPN protocol negotiated during the handshake, or nil if neither
side offered one or none matched.
Returns ?string
TlsStream.peer_certificate()
net.tls.TlsStream.peer_certificate() -> ?PeerCertificate
The peer’s end-entity certificate, or nil if the peer didn’t present
one (only possible on the server side, when
TlsConfig.require_client_cert() wasn’t set).
Returns ?PeerCertificate
TlsStream.to_string()
net.tls.TlsStream.to_string()
2026, Richard Ore and Zuri contributors
net.udp
import net
Everything here is re-exported by
net, soimport netis enough and the names are called asnet.*. Importingnet.udpon its own works too and reaches the same definitions.
This module provides a complete implementation of the User Datagram Protocol as specified in IETF RFC 768. It provides an interface for sending and receiving individual datagrams, including support for broadcast and multicast delivery.
Unlike TCP, UDP is connectionless: there is no handshake, no guarantee of delivery or ordering, and no notion of a byte stream - every send or receive operates on a single, self-contained datagram.
It is meant to be used to provide more controlled and specific UDP-based operating system features and for implementing various standard and custom network protocols and specifications.
The example below shows how to use this module for a simple request/response exchange with a remote host.
import net
var socket = net.UdpSocket()
socket.connect('example.com:9000')
socket.send('ping')
echo socket.receive(512)
socket.close()
A socket can also be bound to a local address to receive datagrams from any sender, as shown below with a basic listener on port 9000.
import net
var server = net.UdpSocket()
server.bind('0.0.0.0:9000')
echo 'listening on ${server.local_address()}'
while true {
echo 'received: ${server.receive_from(512)}'
}
Classes
UdpSocket
class net.UdpSocket
A UDP socket, usable for both sending and receiving datagrams, either
to/from a single fixed peer (after connect()) or to/from arbitrary
addresses (via send_to()/receive_from()/peek_from()).
Unlike a TCP stream, connect() and bind() are not mutually exclusive
here: a socket may be bound to a local address and then separately
connected to a default remote peer.
- printable — has a
@to_string(), soechoandprint()show something useful
Note:
UdpSocket.connect()only narrows which peersend()/recv()talk to and does not perform any handshake.
Note: It’s a good practice to call
close()explicitly once you are done with an instance instead of relying on Zuri to release the socket for you.
Constructor
net.UdpSocket()
Returns a new instance of a UdpSocket, backed by a
fresh, unbound and unconnected native socket. Use bind() to receive
datagrams on a local address, and/or connect() to fix the peer used by
send()/receive().
UdpSocket.connect()
net.UdpSocket.connect(address)
Connects this socket to a remote address. This does not perform a
handshake - UDP is connectionless - it simply records address as the
default peer for future send()/receive()/peek() calls, and filters
out datagrams arriving from any other address. Calling this again
re-targets the socket at a new peer.
address may be a hostname or an IP literal, combined with a port, e.g.
'example.com:9000' or '127.0.0.1:9000'. A hostname may resolve to
several addresses; they are tried in turn and the first one that
succeeds is used.
Parameters
address(string|SocketAddr) — The host:port to treat as the default peer
Raises Error if resolution fails or every resolved address is
rejected
UdpSocket.bind()
net.UdpSocket.bind(address)
Binds this socket to address, allowing it to receive datagrams sent to
that address.
address may be a hostname or an IP literal combined with a port, e.g.
'0.0.0.0:9000' or '[::]:9000'; port 0 asks the OS to assign an
available ephemeral port, which can then be discovered via
local_address(). If address resolves to multiple addresses, binding
is attempted against each in turn until one succeeds.
Parameters
address(string|SocketAddr) — The host:port to bind and receive on
Raises Error if resolution fails or every resolved address fails
to bind (e.g. because it is already in use)
UdpSocket.peer_address()
net.UdpSocket.peer_address() -> SocketAddr
Returns the address of the remote peer this socket was connected to via
connect().
Returns SocketAddr — the peer’s address
Raises Error if this socket has not been connected
UdpSocket.local_address()
net.UdpSocket.local_address() -> SocketAddr
Returns the local socket address of this instance. Particularly useful
for discovering the actual port chosen by the OS after binding to port
0.
Returns SocketAddr — the local address
Raises Error if this socket has not been bound
UdpSocket.set_read_timeout()
net.UdpSocket.set_read_timeout(timeout)
Sets the timeout for future receive()/peek() calls.
Pass a number of milliseconds greater than 0 to bound how long a read
may block before raising a timeout error, or pass 0 (or any negative
number) to remove the timeout entirely and block indefinitely.
Parameters
timeout(number) — Milliseconds, or a negative number to disable the timeout
Raises Error if the platform rejects the value
Note: Some platforms do not provide access to the current timeout.
UdpSocket.get_read_timeout()
net.UdpSocket.get_read_timeout() -> number
Returns the currently configured read timeout in milliseconds, or -1
if no timeout is set (i.e. reads block indefinitely).
Returns number — milliseconds, or -1 if unset
UdpSocket.set_write_timeout()
net.UdpSocket.set_write_timeout(timeout)
Sets the timeout for future send() calls. Same rules as
set_read_timeout(): a number of milliseconds greater than 0 bounds
how long a send may block, 0 (or any negative number) disables the
timeout.
Parameters
timeout(number) — Milliseconds, or a negative number to disable the timeout
Raises Error if the platform rejects the value
Note: A send only blocks in the first place when the OS’s outgoing socket buffer is full, which is uncommon for datagram sockets.
UdpSocket.get_write_timeout()
net.UdpSocket.get_write_timeout() -> number
Returns the currently configured write timeout in milliseconds, or -1
if no timeout is set (i.e. sends block indefinitely).
Returns number — milliseconds, or -1 if unset
UdpSocket.peek()
net.UdpSocket.peek(length) -> bytes
Peeks at the next incoming datagram from the connected peer without
removing it from the socket’s receive queue - a subsequent
receive()/peek() call will still see it. Defaults to peeking a
single byte when length is omitted.
Requires a connected socket, since (like receive()) it only reads from
the peer set via connect(). Use peek_from() on a socket that is only
bound.
Parameters
length(number) — The maximum number of bytes to peek
Returns bytes — up to length bytes of the pending datagram
Raises Error on an underlying I/O error, or if this socket has not
been connected
UdpSocket.peek_from()
net.UdpSocket.peek_from(length) -> data: bytes, address: SocketAddr
Peeks at the next incoming datagram from any sender without removing it
from the socket’s receive queue - a subsequent
receive_from()/peek_from() call will still see it. Defaults to
peeking a single byte when length is omitted. Unlike peek(), this
does not require a connected socket.
Parameters
length(number) — The maximum number of bytes to peek
Returns data: bytes, address: SocketAddr — the pending datagram’s
bytes and the address it was sent from
Raises Error on an underlying I/O error
UdpSocket.set_ttl()
net.UdpSocket.set_ttl(ttl)
Sets IP_TTL (the IPv4 time-to-live / IPv6 hop limit) used for regular,
non-multicast datagrams sent on this socket.
Parameters
ttl(number) — The time-to-live value,0-255
Raises Error on an underlying I/O error
UdpSocket.get_ttl()
net.UdpSocket.get_ttl() -> number
Returns the currently configured IP_TTL value for this socket.
Returns number
Raises Error on an underlying I/O error
UdpSocket.get_error()
net.UdpSocket.get_error() -> ?string
Retrieves and clears the value of the socket’s SO_ERROR option - i.e.
any pending error the OS has recorded for this socket without yet
surfacing through a failed send or receive.
Returns ?string — a description of the pending error, or nil if
there is none
Raises Error on an underlying I/O error while querying the socket
UdpSocket.set_non_blocking()
net.UdpSocket.set_non_blocking(nonblocking)
Puts the socket into or out of non-blocking mode. In non-blocking mode,
receive()/receive_from()/send() and friends return immediately
with an error instead of blocking when the operation would otherwise
wait on I/O.
Parameters
nonblocking(bool) —trueto enable non-blocking mode
Raises Error on an underlying I/O error
UdpSocket.set_broadcast()
net.UdpSocket.set_broadcast(broadcast)
Enables or disables SO_BROADCAST. This must be enabled before
send_to() can deliver a datagram to a broadcast address, such as
255.255.255.255.
Parameters
broadcast(bool) —trueto allow sending to broadcast addresses
Raises Error on an underlying I/O error
UdpSocket.get_broadcast()
net.UdpSocket.get_broadcast() -> bool
Returns whether SO_BROADCAST is currently enabled on this socket.
Returns bool
UdpSocket.set_multicast_loop_v4()
net.UdpSocket.set_multicast_loop_v4(loop)
Enables or disables IP_MULTICAST_LOOP for IPv4 multicast. When enabled
(the default), multicast datagrams sent from this socket are also looped
back and delivered to this same host if it has joined the destination
group.
Parameters
loop(bool) —trueto loop sent multicast datagrams back
Raises Error on an underlying I/O error
UdpSocket.get_multicast_loop_v4()
net.UdpSocket.get_multicast_loop_v4() -> bool
Returns whether IP_MULTICAST_LOOP is currently enabled for IPv4
multicast on this socket.
Returns bool
UdpSocket.set_multicast_ttl_v4()
net.UdpSocket.set_multicast_ttl_v4(ttl)
Sets IP_MULTICAST_TTL, the time-to-live used specifically for outgoing
IPv4 multicast datagrams sent on this socket, independent of the regular
IP_TTL set via set_ttl(). Defaults to 1, restricting multicast
datagrams to the local network.
Parameters
ttl(number) — The multicast time-to-live value,0-255
Raises Error on an underlying I/O error
UdpSocket.get_multicast_ttl_v4()
net.UdpSocket.get_multicast_ttl_v4() -> number
Returns the currently configured IP_MULTICAST_TTL value for IPv4
multicast on this socket.
Returns number
Raises Error on an underlying I/O error
UdpSocket.set_multicast_loop_v6()
net.UdpSocket.set_multicast_loop_v6(loop)
Enables or disables IPV6_MULTICAST_LOOP for IPv6 multicast. When
enabled (the default), multicast datagrams sent from this socket are
also looped back and delivered to this same host if it has joined the
destination group.
Parameters
loop(bool) —trueto loop sent multicast datagrams back
Raises Error on an underlying I/O error
UdpSocket.get_multicast_loop_v6()
net.UdpSocket.get_multicast_loop_v6() -> bool
Returns whether IPV6_MULTICAST_LOOP is currently enabled for IPv6
multicast on this socket.
Returns bool
UdpSocket.receive()
net.UdpSocket.receive(length) -> bytes
Receives a single datagram from the connected peer into a newly
allocated byte buffer of up to length bytes. Requires a connected
socket, since (like peek()) it only reads from the peer set via
connect(). Use receive_from() on a socket that is only bound.
An empty result means an empty (zero-length) datagram was received, which is valid and distinct from there being nothing to receive.
Parameters
length(number) — The maximum number of bytes to read
Returns bytes — up to length bytes of the received datagram
Raises Error on an underlying I/O error, or if the configured read
timeout elapses first
Note: A datagram read is all-or-nothing per message: if the incoming datagram is larger than
length, the excess bytes are discarded rather than being returned on a subsequent call - this is unlike a TCP stream, where a short read just leaves the rest to be read later.
UdpSocket.receive_from()
net.UdpSocket.receive_from(length) -> bytes
Receives a single datagram from any sender into a newly allocated byte
buffer of length bytes. Does not require a connected socket.
Parameters
length(number) — The size of the buffer to read into
Returns bytes — a length-byte buffer containing the received
datagram
Raises Error on an underlying I/O error, or if the configured read
timeout elapses first
Note: As with
receive(), if the incoming datagram is larger thanlength, the excess bytes are discarded.
UdpSocket.send()
net.UdpSocket.send(data) -> number
Sends data as a single datagram to the connected peer. Requires a
connected socket. Unlike a TCP stream’s write(), a successful call
always sends the entirety of data as one datagram - there is no
concept of a partial send here, though very large datagrams may be
rejected outright depending on the platform and network path.
Parameters
data(bytes|string) — The bytes (or UTF-8 text) to send
Returns number — the number of bytes sent (equal to the size of
data on success)
Raises Error on an underlying I/O error, or if the configured
write timeout elapses first
UdpSocket.send_to()
net.UdpSocket.send_to(data, address) -> number
Sends data as a single datagram to address. Does not require a
connected socket, and does not change the peer set by connect(), if
any.
Parameters
data(bytes|string) — The bytes (or UTF-8 text) to sendaddress(string|SocketAddr) — The host:port to send the datagram to
Returns number — the number of bytes sent (equal to the size of
data on success)
Raises Error on an underlying I/O error, or if the configured
write timeout elapses first
UdpSocket.join_multicast_v4()
net.UdpSocket.join_multicast_v4(multiaddr, interface)
Joins the IPv4 multicast group multiaddr on the network interface
identified by interface.
Parameters
multiaddr(string) — The multicast address to join, e.g.'239.0.0.1'interface(string) — The local IPv4 address of the interface to join on, e.g.'0.0.0.0'for the default interface
Raises Error if multiaddr/interface cannot be parsed as IPv4
addresses, or on an underlying I/O error
UdpSocket.leave_multicast_v4()
net.UdpSocket.leave_multicast_v4(multiaddr, interface)
Leaves the IPv4 multicast group multiaddr on the network interface
identified by interface. Both arguments must match the values
originally passed to join_multicast_v4().
Parameters
multiaddr(string) — The multicast address to leaveinterface(string) — The local IPv4 address of the interface to leave on
Raises Error if multiaddr/interface cannot be parsed as IPv4
addresses, or on an underlying I/O error
UdpSocket.join_multicast_v6()
net.UdpSocket.join_multicast_v6(multiaddr, interface)
Joins the IPv6 multicast group multiaddr on the network interface
identified by interface.
Parameters
multiaddr(string) — The multicast address to join, e.g.'ff02::1'interface(number) — The index of the local interface to join on, or0to let the OS choose the default interface
Raises Error if multiaddr cannot be parsed as an IPv6 address,
or on an underlying I/O error
UdpSocket.leave_multicast_v6()
net.UdpSocket.leave_multicast_v6(multiaddr, interface)
Leaves the IPv6 multicast group multiaddr on the network interface
identified by interface. Both arguments must match the values
originally passed to join_multicast_v6().
Parameters
multiaddr(string) — The multicast address to leaveinterface(number) — The index of the local interface to leave on
Raises Error if multiaddr cannot be parsed as an IPv6 address,
or on an underlying I/O error
UdpSocket.close()
net.UdpSocket.close()
Releases the underlying socket. It is advised to call this explicitly once you are done with the socket rather than relying on the Zuri garbage collector to do it for you eventually. Safe to call more than once, and safe to call on an instance that was never bound or connected.
UdpSocket.to_string()
net.UdpSocket.to_string() -> string
A human readable representation of this socket, showing whichever of the bound-local or connected-peer addresses apply.
Returns string
2026, Richard Ore and Zuri contributors
net.unix
import net
Everything here is re-exported by
net, soimport netis enough and the names are called asnet.*. Importingnet.unixon its own works too and reaches the same definitions.
Unix domain sockets: the same stream interface as tcp, over a path on
the filesystem rather than an address on the network.
A unix socket is how two programs on one machine usually talk when one of them is a server. It skips the network stack, so there is no port to collide with and nothing to reach it from another host. And because the socket is a file, the filesystem decides who may connect: a socket in a directory only one user can enter is reachable by only that user, which is a stronger answer than binding to loopback and trusting everyone on the machine.
import net
var client = net.UnixStream()
client.connect('/var/run/mysqld/mysqld.sock')
client.write_all('hello')
echo client.read(5)
client.close()
A server binds a path and accepts on it:
import net
import os
var server = net.UnixStream()
server.bind('/run/app.sock')
while true {
var client = server.accept()
client.write_all('hello\n')
client.close()
}
Binding leaves a file behind
bind() creates the path and refuses if something is already there,
including a socket left by a process that has since died. Closing the
socket frees the descriptor but does not remove the file, because by
then another process may have bound the same path and removing it would
break them. A server that expects to be restarted deletes a stale path
itself before binding.
Windows
Windows has unix sockets, but they are not reachable from Zuri. Every
call here raises there rather than the module being absent, so a program
fails where it tries to connect, saying why, instead of failing at
import. is_supported() answers the question in advance.
Functions
is_supported()
net.is_supported() -> bool
Whether unix domain sockets work on this machine.
False on Windows and true everywhere else. A program that can fall back to TCP tests this rather than catching the error.
Returns bool
pair()
net.pair() -> list[UnixStream]
Two sockets already connected to each other, with no path and nothing on the filesystem.
This is the way to hand one end to a child process, or to talk between threads over a real socket without choosing a name that something else might collide with.
Returns list[UnixStream] — The two ends, which are
interchangeable.
Raises Error if the pair cannot be created.
Classes
UnixStream
class net.UnixStream
A unix domain socket, connected or listening.
One class covers both, as TcpStream does: connect() makes it a
stream and bind() makes it a listener, and the methods that do not
apply raise rather than doing something surprising.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
net.UnixStream(_ptr)
Parameters
_ptr(Ptr|nil) — An existing native handle, asaccept()andpair()produce. A new socket when nil.
UnixStream.native_ptr()
net.UnixStream.native_ptr() -> Ptr
Hands out the underlying native handle, the same way TcpStream does,
so a sibling module can wrap an already-connected socket without
belonging to this class.
Returns Ptr
UnixStream.connect()
net.UnixStream.connect(path: string)
Connects to the socket at path.
There is no timeout: a unix socket connect either succeeds at once or
fails at once, since there is no network in between. Use
set_read_timeout() and set_write_timeout() to bound the conversation
that follows.
Parameters
path(string)
Raises Error if nothing is listening there, if the path does not
exist, or if this process may not open it.
UnixStream.bind()
net.UnixStream.bind(path: string)
Creates the socket at path and starts listening on it.
Parameters
path(string)
Raises Error if something already exists at that path, including a
socket a dead process left behind, or if the directory is not writable.
UnixStream.accept()
net.UnixStream.accept() -> ?UnixStream
Waits for a connection and returns it.
Blocks until someone connects. In non-blocking mode (see
set_non_blocking()) it never blocks: it returns nil when nobody is
waiting, which is an answer rather than a failure.
The accepted stream can carry its own blocking mode, which is a
platform’s default rather than this listener’s. Set it explicitly with
set_non_blocking() on the returned stream if it matters.
Returns ?UnixStream — The connected end of the new conversation,
or nil in non-blocking mode with nobody waiting.
Raises Error if this socket is not listening.
UnixStream.peer_address()
net.UnixStream.peer_address() -> string|nil
The path the other end is bound to.
nil for a socket that has no path, which is what both ends of a
pair() are and what a client that never bound one is.
Returns string|nil
UnixStream.local_address()
net.UnixStream.local_address() -> string|nil
The path this socket is bound to, or nil where it has none.
Returns string|nil
UnixStream.shutdown()
net.UnixStream.shutdown()
Closes both directions without releasing the handle, so a read at the other end returns nothing rather than blocking.
Raises Error if this socket is not connected.
UnixStream.set_read_timeout()
net.UnixStream.set_read_timeout(timeout: number)
Gives up on a read that takes longer than timeout.
Parameters
timeout(number) — Milliseconds. Zero means wait forever.
UnixStream.get_read_timeout()
net.UnixStream.get_read_timeout() -> number
The read timeout in milliseconds, zero for none.
Returns number
UnixStream.set_write_timeout()
net.UnixStream.set_write_timeout(timeout: number)
Gives up on a write that takes longer than timeout.
Parameters
timeout(number) — Milliseconds. Zero means wait forever.
UnixStream.get_write_timeout()
net.UnixStream.get_write_timeout() -> number
The write timeout in milliseconds, zero for none.
Returns number
UnixStream.set_non_blocking()
net.UnixStream.set_non_blocking(nonblocking: bool)
Whether reads and writes return at once rather than waiting.
A non-blocking socket raises where it would have blocked, which is what
net.poll is for: wait there, then read here.
Parameters
nonblocking(bool)
UnixStream.read()
net.UnixStream.read(length: number) -> bytes
Reads up to length bytes, returning what arrived.
A shorter result than asked for is normal and does not mean the conversation is over; an empty one does.
Parameters
length(number)
Returns bytes
UnixStream.read_exact()
net.UnixStream.read_exact(length: number) -> bytes
Reads exactly length bytes, waiting for as many as it takes.
Parameters
length(number)
Returns bytes
Raises Error if the other end closes before that many arrive.
UnixStream.read_all()
net.UnixStream.read_all() -> bytes
Reads until the other end closes.
Returns bytes
UnixStream.read_as_string()
net.UnixStream.read_as_string() -> string
Reads until the other end closes and decodes the result as text.
Returns string
Raises Error if what arrived is not valid UTF-8.
UnixStream.write()
net.UnixStream.write(data) -> number
Writes what it can and reports how much that was.
Parameters
data(bytes|string)
Returns number — Bytes written, which may be fewer than given.
UnixStream.write_all()
net.UnixStream.write_all(data)
Writes everything, however many attempts that takes.
Parameters
data(bytes|string)
UnixStream.flush()
net.UnixStream.flush()
Pushes out anything held back.
UnixStream.is_connected()
net.UnixStream.is_connected() -> bool
Whether this socket is connected to another.
Returns bool
UnixStream.is_bound()
net.UnixStream.is_bound() -> bool
Whether this socket is listening on a path.
Returns bool
UnixStream.close()
net.UnixStream.close()
Closes the socket. The handle cannot be used afterwards.
A path this socket was bound to stays on the filesystem, since by now another process may have bound it.
UnixStream.to_string()
net.UnixStream.to_string()
2026, Richard Ore and Zuri contributors
http
import http
A complete HTTP stack: a client, a server, and the pieces both are built from.
The server is meant to face the internet directly. Everything that is normally the reverse proxy’s job - TLS, static files with byte ranges and conditional requests, response compression, keep-alive, request size limits, timeouts, forwarding-header handling - is here rather than assumed to be somewhere in front.
Making a request
import http
echo http.get('https://example.com').as_text()
The module-level get(), post(), put(), patch(), delete(),
head(), options() and trace() all go through one shared client,
which keeps its connections open between calls. For anything that needs
its own settings - a base URL, an authorization header, a cookie jar, a
custom certificate authority
- build a client of your own:
var api = http.client('https://api.example.com', {
headers: { 'Authorization': 'Bearer ' + token },
})
var user = api.get('/users/me').raise_for_status().as_dict()
api.post('/posts', { title: 'Hello', body: 'World' })
Serving requests
import http
var server = http.server(3000)
server.get('/', @(request, response) {
response.html('<h1>Hello</h1>')
})
server.get('/users/:id', @(request, response) {
response.json({ id: request.param('id') })
})
server.serve_files('/static', './public', { cache_age: 86400 })
server.listen()
Routes match literal segments, :name parameters, and a trailing
catch-all, with the most specific pattern winning regardless of
registration order. use() adds middleware, which runs outermost first
and controls whether the rest of the chain runs at all:
server.use(@(request, response, next) {
if request.header('x-api-key') != key {
response.json({ error: 'unauthorized' }, 401)
return
}
next()
})
Serving over TLS
var server = http.server(443, '0.0.0.0')
server.load_certs('/etc/certs/site.crt', '/etc/certs/site.key')
server.listen()
Using more than one core
listen() serves connections on the calling thread. serve() runs the
same pipeline across a pool of isolates instead, one accept loop handing
connections to workers:
# app.zu
import http
def setup(server) {
server.get('/', @(request, response) {
response.text('hello from a worker')
})
}
# main.zu
import http
import .app
http.serve(app.setup, { port: 3000, workers: 8 })
setup runs inside each worker, so everything it reaches for has to be
something an isolate can be handed. A module is not: a setup that
names an imported module belongs in a module itself, which the isolate
resolves by name and whose own imports are resolved again on that side.
What the module handles for you
Requests are parsed strictly: a header name with whitespace before its
colon, two disagreeing Content-Length values, a Transfer-Encoding
alongside a Content-Length, an obsolete folded header line - each of
these is a way to make a proxy and an origin server disagree about where
one message ends and the next begins, and each is refused rather than
guessed at.
Bodies are decompressed on the way in and compressed on the way out, cookies are parsed and rendered per RFC 6265, dates are read in all three formats HTTP allows and written in the one it requires, and content negotiation follows the quality weights the client actually sent.
Sessions
http.session keeps per-visitor state on the server and finds it again
by a cookie:
import http.session
server.use(http.session.session())
server.get('/', @(request, response) {
var seen = request.session().get('seen', 0)
request.session().set('seen', seen + 1)
response.text('visit ${seen + 1}')
})
Sessions are kept in files by default, in a database through
http.session.sql, or anywhere else a SessionStore can reach.
The http API
Every public name in http, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
http.BodyReader | class | Reads a message body off a connection according to whichever of HTTP/1.1’s framings applies. |
http.ByteRange | class | One byte range asked for by a Range header, already resolved against the size of the representation. |
http.Connection | class | A buffered, message-oriented view of a connected socket. |
http.ConnectionError | class | Raised when a connection could not be established, or when an established connection failed or was closed… |
http.Cookie | class | A single cookie, in either direction: the name/value pair a client sends back in a Cookie header, or the… |
http.CookieJar | class | A client-side cookie store: keeps the cookies a server set, decides which of them a later request is entitled… |
http.Headers | class | An ordered, case-insensitive, multi-value collection of HTTP header fields. |
http.HttpClient | class | An HTTP client. |
http.HttpError | class | Base class for every error the http module raises. |
http.HttpRequest | class | An HTTP request, in both directions. |
http.HttpResponse | class | An HTTP response, in both directions: the thing a server builds and sends, and the thing a client receives… |
http.HttpServer | class | An HTTP/1.1 server. |
http.LoadBalancer | class | Balances requests across several upstreams. |
http.MultipartBuilder | class | Builds a multipart/form-data request body. |
http.MultipartData | class | The result of parsing a multipart/form-data body. |
http.ProtocolError | class | Raised when a peer sends something that isn’t a well-formed HTTP message: a broken request line or status… |
http.ReverseProxy | class | A reverse proxy: takes a request this server received and passes it to an upstream, then passes the… |
http.Route | class | One registered route. |
http.Router | class | Matches request paths to handlers. |
http.StaticFiles | class | Serves files from a directory, with the conditional-request, range-request and caching behaviour a browser… |
http.StatusError | class | Raised by HttpResponse.raise_for_status() when the response carries a 4xx or 5xx status. |
http.TimeoutError | class | Raised when an operation exceeded its configured deadline: a connect, a read, a write, or the total time… |
http.TooLargeError | class | Raised when a message exceeds one of the configured size limits - request line, header block, or body. |
http.TooManyRedirectsError | class | Raised by the client when a redirect chain exceeds HttpClient.max_redirects, which usually means the chain… |
http.UnsupportedProtocolError | class | Raised when a URL names a scheme this module cannot speak, or when a peer insists on a protocol version that… |
http.UploadedFile | class | One file received in a multipart/form-data body. |
http.WebSocket | class | An open WebSocket connection (RFC 6455). |
http.body.DEFAULT_BROTLI_QUALITY | constant | The brotli quality this module compresses a response with when the caller names no quality of its own. |
http.body.decode_content | function | Reverses the content codings named in a Content-Encoding field, innermost last, as RFC 9110 §8.4 requires. |
http.body.encode_chunk | function | Wraps data as a single HTTP/1.1 chunk: the size in hexadecimal, a CRLF, the data, and a CRLF. |
http.body.encode_content | function | Applies a content coding to data. |
http.body.encode_last_chunk | function | The terminating 0\r\n chunk plus a trailer section. |
http.body.parse_chunk_size | function | Parses a chunk-size line, ignoring any chunk extensions after the ;. |
http.body.parse_content_length | function | Parses a Content-Length field value. |
http.body.reader_for | function | Works out how a message’s body is framed and returns a reader for it. |
http.build_query_string | function | Encodes parameters as an application/x-www-form-urlencoded string. |
http.client | function | Builds a new HttpClient. |
http.cookies.format_cookie_header | function | Renders a dictionary of name to value as a request Cookie header value. |
http.cookies.is_valid_value | function | Whether value can be sent as a cookie value without quoting. |
http.cookies.parse_cookie_header | function | Parses a request’s Cookie header into a dictionary of name to value. |
http.cookies.parse_set_cookie | function | Parses one Set-Cookie field value into a Cookie. |
http.delete | function | Sends a DELETE request through the shared client. |
http.files.etag_matches | function | Whether an If-None-Match value matches etag. |
http.files.parse_range | function | Parses a Range header against a representation of size bytes. |
http.files.weak_etag | function | A weak validator derived from a file’s size and modification time. |
http.get | function | Sends a GET request through the shared client. |
http.h1.ChunkWriter | class | The writer handed to a streaming response body. |
http.h1.DEFAULT_LIMITS | constant | |
http.h1.SERVER_NAME | constant | |
http.h1.USER_AGENT | constant | |
http.h1.parse_version | function | Parses HTTP/1.1 into '1.1', rejecting anything that is not a version this module understands the framing… |
http.h1.read_request | function | Reads one request off a connection: the request line, the header section, and enough framing information to… |
http.h1.read_response | function | Reads a response off a connection. |
http.h1.send_continue | function | Sends a 100 Continue interim response, telling a client that asked with Expect: 100-continue to go ahead… |
http.h1.should_keep_alive | function | Whether the connection should be kept open after this exchange. |
http.h1.write_request | function | Writes a request to a connection. |
http.h1.write_response | function | Writes a response to a connection and returns whether the connection is still usable afterwards. |
http.h2.Http2Connection | class | An HTTP/2 connection, in either role. |
http.h2.Http2Stream | class | One HTTP/2 stream: a request and its response, multiplexed with others over a single connection. |
http.h2.Http2Writer | class | The writer a streaming response body gets on an HTTP/2 connection. |
http.h2.frames.CANCEL | constant | CANCEL. |
http.h2.frames.COMPRESSION_ERROR | constant | COMPRESSION_ERROR. |
http.h2.frames.CONNECT_ERROR | constant | CONNECT_ERROR. |
http.h2.frames.CONTINUATION | constant | CONTINUATION frame. |
http.h2.frames.DATA | constant | DATA frame. |
http.h2.frames.ENHANCE_YOUR_CALM | constant | ENHANCE_YOUR_CALM. |
http.h2.frames.FLAG_ACK | constant | ACK flag, on SETTINGS and PING. |
http.h2.frames.FLAG_END_HEADERS | constant | END_HEADERS flag, on HEADERS, PUSH_PROMISE and CONTINUATION. |
http.h2.frames.FLAG_END_STREAM | constant | END_STREAM flag, on DATA and HEADERS. |
http.h2.frames.FLAG_PADDED | constant | PADDED flag, on DATA, HEADERS and PUSH_PROMISE. |
http.h2.frames.FLAG_PRIORITY | constant | PRIORITY flag, on HEADERS. |
http.h2.frames.FLOW_CONTROL_ERROR | constant | FLOW_CONTROL_ERROR. |
http.h2.frames.FRAME_SIZE_ERROR | constant | FRAME_SIZE_ERROR. |
http.h2.frames.Frame | class | One frame, as read off the wire. |
http.h2.frames.GOAWAY | constant | GOAWAY frame. |
http.h2.frames.HEADERS | constant | HEADERS frame. |
http.h2.frames.HTTP_1_1_REQUIRED | constant | HTTP_1_1_REQUIRED. |
http.h2.frames.INADEQUATE_SECURITY | constant | INADEQUATE_SECURITY. |
http.h2.frames.INTERNAL_ERROR | constant | INTERNAL_ERROR. |
http.h2.frames.NO_ERROR | constant | NO_ERROR. |
http.h2.frames.PING | constant | PING frame. |
http.h2.frames.PREFACE | constant | The connection preface every HTTP/2 client sends before anything else. |
http.h2.frames.PRIORITY | constant | PRIORITY frame; deprecated by RFC 9113 and ignored here. |
http.h2.frames.PROTOCOL_ERROR | constant | PROTOCOL_ERROR. |
http.h2.frames.PUSH_PROMISE | constant | PUSH_PROMISE frame. |
http.h2.frames.REFUSED_STREAM | constant | REFUSED_STREAM. |
http.h2.frames.RST_STREAM | constant | RST_STREAM frame. |
http.h2.frames.SETTINGS | constant | SETTINGS frame. |
http.h2.frames.SETTINGS_ENABLE_PUSH | constant | SETTINGS_ENABLE_PUSH. |
http.h2.frames.SETTINGS_HEADER_TABLE_SIZE | constant | SETTINGS_HEADER_TABLE_SIZE. |
http.h2.frames.SETTINGS_INITIAL_WINDOW_SIZE | constant | SETTINGS_INITIAL_WINDOW_SIZE. |
http.h2.frames.SETTINGS_MAX_CONCURRENT_STREAMS | constant | SETTINGS_MAX_CONCURRENT_STREAMS. |
http.h2.frames.SETTINGS_MAX_FRAME_SIZE | constant | SETTINGS_MAX_FRAME_SIZE. |
http.h2.frames.SETTINGS_MAX_HEADER_LIST_SIZE | constant | SETTINGS_MAX_HEADER_LIST_SIZE. |
http.h2.frames.SETTINGS_TIMEOUT | constant | SETTINGS_TIMEOUT. |
http.h2.frames.STREAM_CLOSED | constant | STREAM_CLOSED. |
http.h2.frames.WINDOW_UPDATE | constant | WINDOW_UPDATE frame. |
http.h2.frames.decode_settings | function | Parses a SETTINGS payload into a dictionary. |
http.h2.frames.encode_error | function | Builds an RST_STREAM payload. |
http.h2.frames.encode_goaway | function | Builds a GOAWAY payload. |
http.h2.frames.encode_settings | function | Builds a SETTINGS payload from a dictionary of identifier to value. |
http.h2.frames.read_frame | function | Reads one frame off a connection. |
http.h2.frames.read_u24 | function | Reads a 24-bit big-endian integer from data at offset. |
http.h2.frames.read_u32 | function | Reads a 32-bit big-endian integer from data at offset. |
http.h2.frames.write_frame | function | Writes one frame to a connection. |
http.h2.frames.write_u32 | function | Appends a 32-bit big-endian integer to out. |
http.h2.hpack.Decoder | class | Decodes header blocks for one direction of one connection. |
http.h2.hpack.DynamicTable | class | The dynamic table half of an HPACK context: the most recently inserted fields, evicted from the far end when… |
http.h2.hpack.Encoder | class | Encodes header blocks for one direction of one connection. |
http.h2.hpack.decode_integer | function | Decodes an integer with an prefix_bits-wide prefix, starting at offset. |
http.h2.hpack.decode_string | function | Decodes a string literal starting at offset. |
http.h2.hpack.encode_integer | function | Encodes an integer with an prefix_bits-wide prefix, per RFC 7541 §5.1. |
http.h2.hpack.encode_string | function | Encodes a string literal, Huffman-coding it when that comes out shorter. |
http.h2.huffman.EOS | constant | The number of the symbol HPACK uses to pad the final byte of a Huffman-encoded string, and which must never… |
http.h2.huffman.decode | function | Decodes a Huffman-encoded string. |
http.h2.huffman.encode | function | Huffman-encodes data, padding the final byte with the leading bits of the EOS code (which are all ones) as… |
http.h2.huffman.encoded_length | function | The number of bytes data would occupy once Huffman-encoded. |
http.head | function | Sends a HEAD request through the shared client. |
http.headers.canonical_name | function | The conventional spelling of a field name, e.g. 'content-type' becomes 'Content-Type' and 'etag'… |
http.headers.is_never_folded | function | Whether a field with this name must be repeated rather than folded into one comma-separated value when… |
http.headers.is_valid_name | function | Whether name is a syntactically valid HTTP field name, i.e. a non-empty RFC 9110 token. |
http.headers.is_valid_value | function | Whether value is a legal HTTP field value. |
http.headers.parse | function | Parses a raw header block - everything between a start line and the blank line that ends the head - into a… |
http.middleware.basic_auth | function | Requires HTTP Basic authentication. |
http.middleware.bearer_auth | function | Requires a bearer token. |
http.middleware.cors | function | Answers CORS preflights and adds the cross-origin headers a browser needs before it will let script read a… |
http.middleware.etag | function | Computes a weak ETag over a finished response body and answers 304 Not Modified when the client already… |
http.middleware.force_https | function | Sends every request that arrived over cleartext to the same URL over HTTPS. |
http.middleware.jwt_auth | function | Requires a valid JSON Web Token, verified by the jwt module. |
http.middleware.logger | function | Writes one line per request once the response is finished. |
http.middleware.parse_basic | function | Parses an HTTP Basic Authorization header into a username and password. |
http.middleware.parse_bearer | function | Parses a Bearer Authorization header into its token. |
http.middleware.rate_limit | function | Limits how many requests one client may make in a window of time. |
http.middleware.request_id | function | Attaches a unique identifier to every request, echoing back one the client supplied so a trace can be… |
http.middleware.security_headers | function | Adds the response headers a browser acts on to harden a page. |
http.multipart.parse | function | Parses a multipart/form-data body (RFC 7578). |
http.negotiate.AcceptEntry | class | One entry of an Accept-style header: the value, its quality weight, and any other parameters it carried. |
http.negotiate.best_match | function | Picks the entry of available the client would most like, or nil when it would accept none of them. |
http.negotiate.names_explicitly | function | Whether header names value outright rather than covering it with a wildcard. |
http.negotiate.parse_accept | function | Parses an Accept, Accept-Encoding, Accept-Language or Accept-Charset header into entries, most… |
http.negotiate.preferred_encoding | function | Picks a content coding for a response, given the request’s Accept-Encoding. |
http.negotiate.preferred_language | function | Picks a language from available using the request’s Accept-Language. |
http.negotiate.quality_of | function | The quality weight header assigns to candidate, honouring wildcards. |
http.options | function | Sends an OPTIONS request through the shared client. |
http.parse_query_string | function | Decodes an application/x-www-form-urlencoded string - a query string, or a form body - into `name ->… |
http.patch | function | Sends a PATCH request through the shared client. |
http.post | function | Sends a POST request through the shared client. |
http.put | function | Sends a PUT request through the shared client. |
http.router.RouteMatch | class | The result of asking a router about a request. |
http.serve | function | Runs a server across a pool of isolates, one per core by default. |
http.server | function | Builds an HttpServer. |
http.session.FORMAT | constant | The payload format this module writes and reads. |
http.session.FileStore | class | Keeps each session in its own file, named after the session’s storage key. |
http.session.ID_LENGTH | constant | How many characters a session identifier has. |
http.session.MemoryStore | class | Keeps sessions in a dictionary, for as long as the isolate that made the store lives. |
http.session.Session | class | One visitor’s session. |
http.session.SessionError | class | Raised when a session cannot be read, written or configured: a storage directory that cannot be created or is… |
http.session.SessionStore | class | What every session store implements. |
http.session.default_directory | function | The directory sessions are kept in when a FileStore is not told where to put them: a private subdirectory… |
http.session.file.DIRECTORY_MODE | constant | |
http.session.file.FILE_MODE | constant | |
http.session.file.SUFFIX | constant | |
http.session.session | function | Middleware that finds each request’s session and writes it back when the response goes out. |
http.session.sql.DEFAULT_TABLE | constant | |
http.session.sql.SqlStore | class | Keeps sessions in one table of a relational database. |
http.session.storage_key | function | The key a store files a session under: the SHA-256 of the session identifier, as 64 lowercase hex characters. |
http.set_headers | function | Sets the default headers on the shared client and returns it, so a call can be chained straight onto it. |
http.shared_client | function | The shared client the module-level request functions use. |
http.sse.EventStream | class | The writer a server-sent event stream hands to its producer. |
http.sse.last_event_id | function | The Last-Event-ID a reconnecting client sent, or nil. |
http.sse.parse | function | Parses a text/event-stream body into a list of events, each a dictionary with event, data, id and… |
http.sse.stream | function | Turns response into a server-sent event stream and runs producer against it. |
http.status.ACCEPTED | constant | 202 Accepted. |
http.status.ALREADY_REPORTED | constant | 208 Already Reported (WebDAV, RFC 5842). |
http.status.BAD_GATEWAY | constant | 502 Bad Gateway. |
http.status.BAD_REQUEST | constant | 400 Bad Request. |
http.status.CONFLICT | constant | 409 Conflict. |
http.status.CONTENT_TOO_LARGE | constant | 413 Content Too Large. |
http.status.CONTINUE | constant | 100 Continue. |
http.status.CREATED | constant | 201 Created. |
http.status.EARLY_HINTS | constant | 103 Early Hints (RFC 8297). |
http.status.EXPECTATION_FAILED | constant | 417 Expectation Failed. |
http.status.FAILED_DEPENDENCY | constant | 424 Failed Dependency (WebDAV, RFC 4918). |
http.status.FORBIDDEN | constant | 403 Forbidden. |
http.status.FOUND | constant | 302 Found. |
http.status.GATEWAY_TIMEOUT | constant | 504 Gateway Timeout. |
http.status.GONE | constant | 410 Gone. |
http.status.HTTP_VERSION_NOT_SUPPORTED | constant | 505 HTTP Version Not Supported. |
http.status.IM_A_TEAPOT | constant | 418 I’m a teapot (RFC 2324). |
http.status.IM_USED | constant | 226 IM Used (RFC 3229). |
http.status.INSUFFICIENT_STORAGE | constant | 507 Insufficient Storage (WebDAV, RFC 4918). |
http.status.INTERNAL_SERVER_ERROR | constant | 500 Internal Server Error. |
http.status.LENGTH_REQUIRED | constant | 411 Length Required. |
http.status.LOCKED | constant | 423 Locked (WebDAV, RFC 4918). |
http.status.LOOP_DETECTED | constant | 508 Loop Detected (WebDAV, RFC 5842). |
http.status.METHOD_NOT_ALLOWED | constant | 405 Method Not Allowed. |
http.status.MISDIRECTED_REQUEST | constant | 421 Misdirected Request. |
http.status.MOVED_PERMANENTLY | constant | 301 Moved Permanently. |
http.status.MULTIPLE_CHOICES | constant | 300 Multiple Choices. |
http.status.MULTI_STATUS | constant | 207 Multi-Status (WebDAV, RFC 4918). |
http.status.NETWORK_AUTHENTICATION_REQUIRED | constant | 511 Network Authentication Required (RFC 6585). |
http.status.NON_AUTHORITATIVE_INFORMATION | constant | 203 Non-Authoritative Information. |
http.status.NOT_ACCEPTABLE | constant | 406 Not Acceptable. |
http.status.NOT_EXTENDED | constant | 510 Not Extended (RFC 2774). |
http.status.NOT_FOUND | constant | 404 Not Found. |
http.status.NOT_IMPLEMENTED | constant | 501 Not Implemented. |
http.status.NOT_MODIFIED | constant | 304 Not Modified. |
http.status.NO_CONTENT | constant | 204 No Content. |
http.status.OK | constant | 200 OK. |
http.status.PARTIAL_CONTENT | constant | 206 Partial Content. |
http.status.PAYMENT_REQUIRED | constant | 402 Payment Required. |
http.status.PERMANENT_REDIRECT | constant | 308 Permanent Redirect. |
http.status.PRECONDITION_FAILED | constant | 412 Precondition Failed. |
http.status.PRECONDITION_REQUIRED | constant | 428 Precondition Required (RFC 6585). |
http.status.PROCESSING | constant | 102 Processing (WebDAV, RFC 2518). |
http.status.PROXY_AUTHENTICATION_REQUIRED | constant | 407 Proxy Authentication Required. |
http.status.RANGE_NOT_SATISFIABLE | constant | 416 Range Not Satisfiable. |
http.status.REQUEST_HEADER_FIELDS_TOO_LARGE | constant | 431 Request Header Fields Too Large (RFC 6585). |
http.status.REQUEST_TIMEOUT | constant | 408 Request Timeout. |
http.status.RESET_CONTENT | constant | 205 Reset Content. |
http.status.SEE_OTHER | constant | 303 See Other. |
http.status.SERVICE_UNAVAILABLE | constant | 503 Service Unavailable. |
http.status.SWITCHING_PROTOCOLS | constant | 101 Switching Protocols. |
http.status.TEMPORARY_REDIRECT | constant | 307 Temporary Redirect. |
http.status.TOO_EARLY | constant | 425 Too Early (RFC 8470). |
http.status.TOO_MANY_REQUESTS | constant | 429 Too Many Requests (RFC 6585). |
http.status.UNAUTHORIZED | constant | 401 Unauthorized. |
http.status.UNAVAILABLE_FOR_LEGAL_REASONS | constant | 451 Unavailable For Legal Reasons (RFC 7725). |
http.status.UNPROCESSABLE_CONTENT | constant | 422 Unprocessable Content. |
http.status.UNSUPPORTED_MEDIA_TYPE | constant | 415 Unsupported Media Type. |
http.status.UPGRADE_REQUIRED | constant | 426 Upgrade Required. |
http.status.URI_TOO_LONG | constant | 414 URI Too Long. |
http.status.USE_PROXY | constant | 305 Use Proxy. |
http.status.VARIANT_ALSO_NEGOTIATES | constant | 506 Variant Also Negotiates (RFC 2295). |
http.status.is_bodiless | function | Whether a response carrying code is defined to have no body at all, regardless of what headers say. |
http.status.is_client_error | function | Whether code is a 4xx client error. |
http.status.is_error | function | Whether code is any kind of error, client or server. |
http.status.is_informational | function | Whether code is a 1xx interim status. |
http.status.is_redirect | function | Whether code is a 3xx redirection status. |
http.status.is_registered | function | Whether code is a registered status code with a canonical reason phrase of its own. |
http.status.is_server_error | function | Whether code is a 5xx server error. |
http.status.is_success | function | Whether code is a 2xx success status. |
http.status.preserves_method | function | Whether a redirect with code must keep the original method and body when followed. |
http.status.reason | function | The canonical reason phrase for code, e.g. 'Not Found' for 404. |
http.stream.accept | function | Turns a freshly accepted TcpStream into a Connection, running a server-side TLS handshake first when… |
http.stream.connect | function | Opens a connection to host on port, optionally wrapping it in TLS. |
http.stream.is_timeout_error | function | Whether a transport error message describes a timeout (or a would-block, which a socket with a receive… |
http.stream.tunnel | function | Opens a connection to host:port through an HTTP proxy’s CONNECT tunnel, running a TLS handshake with… |
http.tls_server | function | Builds an HttpServer already configured for TLS. |
http.trace | function | Sends a TRACE request through the shared client. |
http.util.find_bytes | function | Finds the first occurrence of the byte sequence needle in haystack, at or after from, or -1 when it… |
http.util.format_date | function | Formats a Unix timestamp as an IMF-fixdate, the one date format RFC 9110 §5.6.7 requires every HTTP sender to… |
http.util.is_valid_method | function | Whether value is a valid HTTP method: a non-empty token, per RFC 9110 §9. |
http.util.normalize_path | function | Resolves the . and .. segments of a path and collapses repeated slashes, returning a path that cannot… |
http.util.parse_basic | function | Parses an HTTP Basic Authorization field value into a username and password. |
http.util.parse_bearer | function | Parses a Bearer Authorization field value into its token. |
http.util.parse_date | function | Parses any of the three date formats RFC 9110 §5.6.7 requires a recipient to accept, and returns the Unix… |
http.util.parse_parameters | function | Splits a header value that carries parameters - a media type, a Content-Disposition, a challenge - into its… |
http.util.percent_decode | function | Percent-decodes a URI component, turning + into a space only when plus_as_space is set - which is right… |
http.util.percent_encode | function | Percent-encodes every character of text that is not an RFC 3986 unreserved character, over the UTF-8… |
http.util.quote | function | Wraps value in double quotes, escaping any quote or backslash it contains, so it can be used as an RFC 9110… |
http.util.random_token | function | A random lowercase-hex token of length characters, drawn from the platform’s cryptographically secure… |
http.util.secure_equals | function | Compares two strings without leaking, through how long the comparison takes, where they first differ. |
http.util.to_hex | function | n as lowercase hexadecimal, with no 0x prefix and no padding. |
http.util.unquote | function | Removes the surrounding double quotes from a header value and resolves its backslash escapes. |
http.websocket.CLOSE_GOING_AWAY | constant | The endpoint is going away. |
http.websocket.CLOSE_INTERNAL_ERROR | constant | An unexpected condition on the server. |
http.websocket.CLOSE_INVALID_PAYLOAD | constant | A text message that was not valid UTF-8. |
http.websocket.CLOSE_NORMAL | constant | Normal closure. |
http.websocket.CLOSE_POLICY_VIOLATION | constant | A message that violates a policy. |
http.websocket.CLOSE_PROTOCOL_ERROR | constant | A protocol error was detected. |
http.websocket.CLOSE_TOO_LARGE | constant | A message too large to process. |
http.websocket.CLOSE_UNSUPPORTED | constant | A message of a kind this endpoint cannot accept. |
http.websocket.Message | class | One complete WebSocket message, with any fragmentation already reassembled. |
http.websocket.OPCODE_BINARY | constant | Binary frame. |
http.websocket.OPCODE_CLOSE | constant | Close frame. |
http.websocket.OPCODE_CONTINUATION | constant | Continuation frame. |
http.websocket.OPCODE_PING | constant | Ping frame. |
http.websocket.OPCODE_PONG | constant | Pong frame. |
http.websocket.OPCODE_TEXT | constant | Text frame. |
http.websocket.accept | function | Completes a WebSocket handshake and takes over the connection. |
http.websocket.accept_key | function | The value a server must return in Sec-WebSocket-Accept for a given client key. |
http.websocket.connect | function | Opens a WebSocket connection to target. |
http.websocket.is_handshake | function | Whether request is a well-formed WebSocket handshake. |
http.worker.serve | function | Binds a listening socket and serves it across a pool of worker isolates. |
http.worker.worker_main | function | The loop each worker isolate runs: build a server of its own from setup, then serve whatever connections… |
Submodules
| Module | Reached as | Summary |
|---|---|---|
http.body | http.body.* | A request or response body, in whatever shape the wire delivered it: a fixed Content-Length, a chunked… |
http.client | http.client.* | HttpClient: the connection-pooling, redirect-following, cookie-aware side of the module. |
http.cookies | http.cookies.* | Cookies, both halves of them: Cookie is one cookie with its attributes, CookieJar is a store that applies… |
http.errors | http.* | Every error the HTTP stack raises, under one root. |
http.files | http.files.* | Serving files off disk, with the parts that make it correct rather than merely working: conditional requests,… |
http.h1 | http.h1.* | HTTP/1.1 on the wire (RFC 9110 and RFC 9112): reading a request line and its headers, writing a status line… |
http.h2 | http.h2.* | HTTP/2 (RFC 9113) and the header compression it uses (RFC 7541). |
http.headers | http.headers.* | Headers: a case-insensitive, order-preserving multimap, because HTTP header names do not compare… |
http.middleware | http.middleware.* | The middleware every public HTTP service ends up needing: CORS, access logging, the security headers a… |
http.multipart | http.multipart.* | multipart/form-data, in both directions. |
http.negotiate | http.negotiate.* | Content negotiation: choosing what to send when the client has said what it prefers. |
http.proxy | http.proxy.* | ReverseProxy forwards a request to another server and streams the response back; LoadBalancer spreads… |
http.request | http.request.* | HttpRequest: one inbound request, with its method, target, headers and body, plus the query string and… |
http.response | http.response.* | HttpResponse: one response, whether it is being built by a handler or read back from a server. |
http.router | http.router.* | Matching a request to a handler. |
http.server | http.server.* | HttpServer: the server end of the module. |
http.session | http.session.* | Server-side sessions: a small amount of state that belongs to one visitor, kept on the server and found again… |
http.sse | http.sse.* | Server-sent events (the WHATWG text/event-stream format): a one-way stream of named, identified messages… |
http.status | http.status.* | The HTTP status codes registered with IANA, their canonical reason phrases, and a handful of predicates for… |
http.stream | http.stream.* | The transport underneath everything else: a byte stream with buffering, timeouts and optional TLS. |
http.util | http.util.* | The small, exact pieces of the HTTP specifications that several parts of the module need: date formatting,… |
http.websocket | http.websocket.* | WebSocket (RFC 6455), both ends of it. |
http.worker | http.worker.* | The multi-process side of HttpServer: a pool of isolates, each accepting and serving connections from the… |
Functions
shared_client()
http.shared_client() -> HttpClient
The shared client the module-level request functions use.
Reach for this to change a setting that should apply to every casual
http.get() in a program - a proxy-wide certificate authority, a longer
timeout - and build your own client() for anything more specific than
that.
Returns HttpClient
client()
http.client(base_url: ?string, options: ?dict) -> HttpClient
Builds a new HttpClient.
Parameters
base_url(?string) — prefixed to any relative request targetoptions(?dict) — anyHttpClientfield, plusheaders
Returns HttpClient
set_headers()
http.set_headers(values: dict) -> HttpClient
Sets the default headers on the shared client and returns it, so a call can be chained straight onto it.
echo http.set_headers({ 'Authorization': 'Bearer ' + token })
.get('https://example.com/me')
.as_dict()
Parameters
values(dict)
Returns HttpClient
get()
http.get(url: string, options: ?dict) -> HttpResponse
Sends a GET request through the shared client.
Parameters
url(string)options(?dict) — seeHttpClient.request()
Returns HttpResponse
post()
http.post(url: string, data, options: ?dict) -> HttpResponse
Sends a POST request through the shared client.
Parameters
url(string)data(?any) — a string or bytes sent as-is, a dictionary or list sent as JSON, or aMultipartBuilderoptions(?dict)
Returns HttpResponse
put()
http.put(url: string, data, options: ?dict) -> HttpResponse
Sends a PUT request through the shared client.
Parameters
url(string)data(?any)options(?dict)
Returns HttpResponse
patch()
http.patch(url: string, data, options: ?dict) -> HttpResponse
Sends a PATCH request through the shared client.
Parameters
url(string)data(?any)options(?dict)
Returns HttpResponse
delete()
http.delete(url: string, options: ?dict) -> HttpResponse
Sends a DELETE request through the shared client.
Parameters
url(string)options(?dict)
Returns HttpResponse
head()
http.head(url: string, options: ?dict) -> HttpResponse
Sends a HEAD request through the shared client. Redirects are not
followed unless options says to.
Parameters
url(string)options(?dict)
Returns HttpResponse
options()
http.options(url: string, options: ?dict) -> HttpResponse
Sends an OPTIONS request through the shared client.
Parameters
url(string)options(?dict)
Returns HttpResponse
trace()
http.trace(url: string, options: ?dict) -> HttpResponse
Sends a TRACE request through the shared client.
Parameters
url(string)options(?dict)
Returns HttpResponse
server()
http.server(port: ?number, host: ?string) -> HttpServer
Builds an HttpServer.
Parameters
port(?number) — defaults to8000host(?string) — defaults to'127.0.0.1'
Returns HttpServer
tls_server()
http.tls_server(port: number, cert_chain: string, private_key: string, host: ?string) -> HttpServer
Builds an HttpServer already configured for TLS.
Parameters
port(number)cert_chain(string) — the PEM certificate chain, leaf firstprivate_key(string) — the PEM private keyhost(?string)
Returns HttpServer
serve()
http.serve(setup, options: ?dict)
Runs a server across a pool of isolates, one per core by default.
setup is called once inside each worker with that worker’s own
HttpServer, and registers the routes, middleware and settings the
worker should serve with. It may be defined in the main script or in a
module, and may use whatever it imports; an imported module is reloaded
inside the worker rather than shared with it, so the worker gets its own
copy of that module’s top-level state.
The calling isolate binds the socket and accepts connections, handing each one to a worker. It does not return until the server is stopped.
Parameters
setup(function(1)) — receives the worker’sHttpServeroptions(?dict) —port(default8000),host(default'127.0.0.1'),workers(default: the number of CPUs),backlog(how many accepted connections may wait for a free worker), pluscert_chain/private_keyfor TLS
Raises HttpError if the socket cannot be bound
2026, Richard Ore and Zuri contributors
http.body
import http.body
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.body.*needsimport http.body.
A request or response body, in whatever shape the wire delivered it: a
fixed Content-Length, a chunked stream, or nothing at all.
BodyReader hides that difference behind one interface, and
decode_content()/encode_content() handle the Content-Encoding
layer on top of it: gzip, deflate, brotli and zstd.
Constants
DEFAULT_BROTLI_QUALITY
http.body.DEFAULT_BROTLI_QUALITY: int = 5
The brotli quality this module compresses a response with when the caller names no quality of its own.
Brotli’s own default is 11, and that is a setting for compressing a file once at build time rather than for compressing a response per request. Measured against q5 on real response bodies, q11 costs 64-104x the CPU and, because of how brotli sizes its window at high quality, frequently produces a larger result:
| body | q11 | q5 |
|---|---|---|
| 1.5 KB JSON | 3.48 ms, 152 B | 0.054 ms, 152 B |
| 15 KB JSON | 13.5 ms, 654 B | 0.19 ms, 591 B |
| 150 KB JSON | 144 ms, 5119 B | 2.08 ms, 4831 B |
| 120 KB HTML | 178 ms, 3322 B | 1.88 ms, 3350 B |
A 178 ms compression on a single page is the entire time budget for a request, spent to make the response 0.8% smaller.
Functions
parse_content_length()
http.body.parse_content_length(value) -> number
Parses a Content-Length field value.
RFC 9112 §6.3 is strict here for good reason: a value that isn’t a plain run of digits, or a field carrying two different lengths, is the raw material of request smuggling, where a proxy and an origin read the same bytes as different numbers of messages.
Parameters
value(string)
Returns number
Raises ProtocolError if the value is not a bare decimal number
parse_chunk_size()
http.body.parse_chunk_size(line) -> number
Parses a chunk-size line, ignoring any chunk extensions after the ;.
Parameters
line(string)
Returns number
Raises ProtocolError on anything that isn’t hexadecimal
reader_for()
http.body.reader_for(connection, headers, is_request: bool, bodiless: ?bool) -> BodyReader
Works out how a message’s body is framed and returns a reader for it.
is_request matters because the two directions disagree about what “no
framing headers” means: a request without them has no body, while a
response without them runs until the connection closes.
Parameters
connection(Connection)headers(Headers)is_request(bool)bodiless(?bool) — force an empty body regardless of the headers - true for a response to HEAD, and for1xx,204and304, all of which carry framing headers that describe a body they do not actually send
Returns BodyReader
Raises ProtocolError if the framing headers contradict each other
encode_chunk()
http.body.encode_chunk(data) -> bytes
Wraps data as a single HTTP/1.1 chunk: the size in hexadecimal, a
CRLF, the data, and a CRLF.
Parameters
data(bytes)
Returns bytes
encode_last_chunk()
http.body.encode_last_chunk(trailers) -> bytes
The terminating 0\r\n chunk plus a trailer section.
Parameters
trailers(?Headers)
Returns bytes
decode_content()
http.body.decode_content(data, encoding) -> bytes
Reverses the content codings named in a Content-Encoding field,
innermost last, as RFC 9110 §8.4 requires.
identity is accepted and does nothing. Anything else raises, because
silently handing back still-encoded bytes would look like corrupt data
at the call site.
Parameters
data(bytes)encoding(string) — the raw field value, e.g.'gzip'
Returns bytes
Raises ProtocolError on an unknown coding or undecodable data
encode_content()
http.body.encode_content(data, coding: string, quality: ?number) -> bytes
Applies a content coding to data.
quality is interpreted on the scale of whichever coding is being
applied - brotli 0-11, deflate 0-9, zstd 1-22 - and is
clamped into range rather than rejected. Omit it to get the coding’s own
sensible default for a response compressed per request: 5 for brotli
(see DEFAULT_BROTLI_QUALITY), and each of the others’ native default,
which is already the right shape for dynamic content.
Parameters
data(bytes)coding(string) —'gzip','deflate','br','zstd'or'identity'quality(?number)
Returns bytes
Raises ProtocolError on an unknown coding
Note:
gziphas no quality parameter at this layer. It compresses at zlib’s own default level of 6, which is what nginx and every CDN use for dynamic responses anyway.
Classes
BodyReader
class http.BodyReader
Reads a message body off a connection according to whichever of HTTP/1.1’s framings applies.
The framing is decided once, by for_message(), and never re-derived:
Transfer-Encoding: chunked wins over Content-Length, a fixed length
is next, and a response with neither is delimited by the connection
closing. A request with neither has no body at all - a server may not
wait for a close that a client has no reason to perform.
Read it incrementally with read(), or in one go with read_all().
Either way, call drain() before reusing the connection, or the next
message will start parsing in the middle of this one’s body.
Fields
| Field | Type | Description |
|---|---|---|
mode | string | One of 'empty', 'length', 'chunked' or 'eof'. |
length | number | The declared body length for a 'length' body, or -1 when the length is not known in advance. |
trailers | Headers | Trailer fields, populated once a chunked body has been read to completion. |
Constructor
http.BodyReader(connection, mode: string, length)
Parameters
connection(Connection)mode(string)length(?number)
BodyReader.is_finished()
http.BodyReader.is_finished() -> bool
Whether the body has been read to its end.
Returns bool
BodyReader.read()
http.BodyReader.read(length)
Reads up to length more bytes of the body, or all of what remains when
length is omitted and the body is short.
Parameters
length(?number) — defaults to 64 KiB
Returns — bytes: empty once the body is exhausted
Raises ProtocolError on a malformed chunked body or a truncated
fixed-length one
BodyReader.read_all()
http.BodyReader.read_all(limit) -> bytes
Reads the whole body into memory.
Parameters
limit(?number) — the most to accept, in bytes;nilfor no limit
Returns bytes
Raises TooLargeError if the body exceeds limit
BodyReader.drain()
http.BodyReader.drain(limit)
Reads and throws away whatever is left, so the connection is positioned at the start of the next message and can be reused.
Returns false when the leftover exceeds limit - at which point
draining costs more than a new connection would, and the caller should
close instead.
Parameters
limit(?number) — the most worth draining; defaults to 1 MiB
Returns — bool: whether the connection is now reusable
2026, Richard Ore and Zuri contributors
http.client
import http.client
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.client.*needsimport http.client.
HttpClient: the connection-pooling, redirect-following, cookie-aware
side of the module.
It negotiates HTTP/2 over TLS when the server offers it, falls back to
HTTP/1.1 otherwise, and reuses connections between requests. The
module-level http.get()/http.post() shortcuts are thin wrappers
around a shared instance of this.
Classes
HttpClient
class http.HttpClient
An HTTP client.
import http
var client = http.HttpClient()
var response = client.get('https://example.com')
echo response.status
echo response.as_text()
One client is meant to be kept and reused: it holds the connection pool, the cookie jar, and the TLS configuration, all of which are wasted if a fresh client is built per request. Reusing it also means the second request to a host skips the TCP and TLS handshakes entirely.
Redirects are followed, responses are decompressed, and cookies are carried between requests when a jar is attached - all of which can be turned off.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
base_url | ?string | Prefixed to any request URL that is not already absolute, so a client can be pointed at one service and then… |
headers | Headers | Headers sent with every request, unless a request overrides them. |
user_agent | string | The User-Agent sent unless one is set in headers. |
follow_redirects | bool | Whether to follow 3xx responses. |
max_redirects | number | The most redirects to follow before giving up. |
connect_timeout | number | How long to wait for a connection to be established, in milliseconds. |
read_timeout | number | How long to wait for data once connected, in milliseconds. |
write_timeout | number | How long a write may block, in milliseconds. |
verify | bool | Whether to verify the server’s certificate chain and hostname. |
max_body_size | number | The largest response body accepted, in bytes. |
decode_content | bool | Whether to decode a compressed response body automatically. |
max_retries | number | How many times to retry a request that failed on a connection error before giving up. |
http2 | bool | Whether to negotiate HTTP/2 over TLS. |
proxy | ?string | The HTTP proxy every request goes through, as http://host:port, with user:password@ in front of the host… |
trust_env | bool | Whether to take a proxy from the environment when proxy is not set: HTTPS_PROXY for https addresses,… |
cookie_jar | ?CookieJar | The cookie jar, or nil to not track cookies at all. |
Constructor
http.HttpClient(base_url: ?string, options: ?dict)
Parameters
base_url(?string)options(?dict) — any of the fields above, plusheaders
HttpClient.get()
http.HttpClient.get(target: string, options: ?dict) -> HttpResponse
Sends a GET request.
Parameters
target(string) — an absolute URL, or a path whenbase_urlis setoptions(?dict) — seerequest()
Returns HttpResponse
HttpClient.post()
http.HttpClient.post(target: string, data, options: ?dict) -> HttpResponse
Sends a POST request.
data may be a string, bytes, a dictionary (sent as JSON), or a
MultipartBuilder. Use the form option instead to send a dictionary
as application/x-www-form-urlencoded.
Parameters
target(string)data(?any)options(?dict) — seerequest()
Returns HttpResponse
HttpClient.put()
http.HttpClient.put(target: string, data, options: ?dict) -> HttpResponse
Sends a PUT request.
Parameters
target(string)data(?any)options(?dict)
Returns HttpResponse
HttpClient.patch()
http.HttpClient.patch(target: string, data, options: ?dict) -> HttpResponse
Sends a PATCH request.
Parameters
target(string)data(?any)options(?dict)
Returns HttpResponse
HttpClient.delete()
http.HttpClient.delete(target: string, options: ?dict) -> HttpResponse
Sends a DELETE request.
Parameters
target(string)options(?dict)
Returns HttpResponse
HttpClient.head()
http.HttpClient.head(target: string, options: ?dict) -> HttpResponse
Sends a HEAD request. Redirects are not followed by default here, since the point of a HEAD is usually to inspect the very response a redirect would hide.
Parameters
target(string)options(?dict)
Returns HttpResponse
HttpClient.options()
http.HttpClient.options(target: string, options: ?dict) -> HttpResponse
Sends an OPTIONS request.
Parameters
target(string)options(?dict)
Returns HttpResponse
HttpClient.trace()
http.HttpClient.trace(target: string, options: ?dict) -> HttpResponse
Sends a TRACE request.
Parameters
target(string)options(?dict)
Returns HttpResponse
Note: A TRACE echoes the request back, headers included, which is why servers that sit behind authenticating proxies usually refuse it.
HttpClient.request()
http.HttpClient.request(method: string, target: string, options: ?dict) -> HttpResponse
Sends a request and returns the response.
The options dictionary accepts:
| Option | Meaning |
|---|---|
headers | a dictionary or Headers merged over the client’s |
body | a string or bytes, sent as-is |
json | any value, JSON-encoded, with the matching content type |
form | a dictionary, form-urlencoded |
multipart | a MultipartBuilder |
query | a dictionary merged into the URL’s query string |
auth | ['basic', user, password] or ['bearer', token] |
follow_redirects | overrides the client setting |
timeout | read timeout for this request, in milliseconds |
stream | leave the body unread on response.body_reader |
decode_content | overrides the client setting |
Parameters
method(string)target(string)options(?dict)
Returns HttpResponse
Raises ConnectionError, TimeoutError, ProtocolError,
TooManyRedirectsError, UnsupportedProtocolError
HttpClient.finish()
http.HttpClient.finish(response)
Releases the connection behind a streamed response, once the caller has finished reading its body.
Anything left unread is drained so the connection can be reused; when there is too much left for that to be worth it, the connection is closed instead. Calling this on an ordinary (non-streamed) response does nothing.
Parameters
response(HttpResponse)
HttpClient.add_ca()
http.HttpClient.add_ca(pem: string)
Trusts an additional PEM-encoded certificate authority, on top of the platform’s own roots.
This is the right way to talk to a service with an internal or
self-signed certificate; turning verify off instead trusts everyone,
including whoever is between you and the server.
Parameters
pem(string)
Returns — HttpClient: this same instance, for chaining
HttpClient.set_tls_config()
http.HttpClient.set_tls_config(config)
Uses a net.tls.TlsConfig built elsewhere for every HTTPS connection
this client makes.
Parameters
config(TlsConfig)
Returns — HttpClient: this same instance, for chaining
HttpClient.set_header()
http.HttpClient.set_header(name: string, value)
Sets a header sent with every request from this client.
Parameters
name(string)value(string|number|bool)
Returns — HttpClient: this same instance, for chaining
HttpClient.set_headers()
http.HttpClient.set_headers(values)
Replaces the default headers wholesale.
Parameters
values(dict|Headers)
Returns — HttpClient: this same instance, for chaining
HttpClient.enable_cookies()
http.HttpClient.enable_cookies()
Starts tracking cookies, so a session survives across requests from this client.
Returns — HttpClient: this same instance, for chaining
HttpClient.close()
http.HttpClient.close()
Closes every pooled connection. A client is usable afterwards - the next request simply opens a fresh connection - but the sockets are released, which matters at the end of a long-running process or before forking.
HttpClient.pooled_connections()
http.HttpClient.pooled_connections() -> number
How many connections are currently idle in the pool.
Returns number
HttpClient.to_string()
http.HttpClient.to_string()
2026, Richard Ore and Zuri contributors
http.cookies
import http.cookies
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.cookies.*needsimport http.cookies.
Cookies, both halves of them: Cookie is one cookie with its
attributes, CookieJar is a store that applies the domain, path and
expiry rules when deciding what to send.
The parsing and formatting functions handle the two header formats,
which are not symmetric: a request sends many cookies in one Cookie
header, a response sets one per Set-Cookie.
Functions
is_valid_value()
http.cookies.is_valid_value(value) -> bool
Whether value can be sent as a cookie value without quoting.
Parameters
value(string)
Returns bool
parse_cookie_header()
http.cookies.parse_cookie_header(header) -> dict
Parses a request’s Cookie header into a dictionary of name to value.
A cookie header is a flat a=1; b=2 list with no attributes -
everything the server set is gone by the time it comes back, which is
why a server that needs to know a cookie’s path or expiry has to have
stored that itself.
Malformed pairs are skipped rather than raising: a browser will cheerfully send whatever junk any script on the page stored, and refusing the whole request over one bad pair loses the good ones too.
Parameters
header(string)
Returns dict
parse_set_cookie()
http.cookies.parse_set_cookie(header)
Parses one Set-Cookie field value into a Cookie.
Parameters
header(string)
Returns — ?Cookie: nil when the field carries no usable name/value
pair
format_cookie_header()
http.cookies.format_cookie_header(cookies: dict) -> string
Renders a dictionary of name to value as a request Cookie header
value.
Parameters
cookies(dict)
Returns string
Classes
Cookie
class http.Cookie
A single cookie, in either direction: the name/value pair a client sends
back in a Cookie header, or the full attribute set a server sends in
Set-Cookie.
The attributes follow RFC 6265bis, including SameSite and the
__Host-/__Secure- name prefixes, whose rules are enforced by
to_header() rather than left to the caller to remember.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
name | string | The cookie’s name. |
value | string | The cookie’s value. |
domain | ?string | The domain the cookie is sent to, or nil to scope it to exactly the host that set it. |
path | ?string | The path prefix the cookie is sent for. |
expires | ?number | Absolute expiry, as seconds since the epoch. |
max_age | ?number | Lifetime in seconds from now. |
secure | bool | Whether the cookie is only ever sent over HTTPS. |
http_only | bool | Whether the cookie is hidden from client-side scripts. |
same_site | ?string | 'Strict', 'Lax' or 'None'. |
partitioned | bool | Whether the cookie is partitioned by top-level site (CHIPS). |
Constructor
http.Cookie(name: string, value: string, attributes: ?dict)
Parameters
name(string)value(string)attributes(?dict) — any ofdomain,path,expires,max_age,secure,http_only,same_site,partitioned
Raises ProtocolError if the name is not a token, or the value
contains a character that cannot appear in a cookie
Cookie.to_header()
http.Cookie.to_header() -> string
Renders the cookie as a Set-Cookie field value.
The name prefixes defined in RFC 6265bis §4.1.3 are honoured here: a
__Secure- cookie is forced Secure, and a __Host- cookie is forced
Secure, pinned to Path=/, and stripped of any Domain. Browsers
reject cookies that carry a prefix without meeting its conditions, so
fixing them up beats silently emitting a cookie that never gets stored.
SameSite=None likewise implies Secure, without which the cookie is
dropped.
Returns string
Cookie.is_expired()
http.Cookie.is_expired(now) -> bool
Whether the cookie’s own attributes say it has already expired, as of
now (default: the current time). A session cookie - one with neither
expires nor max_age - is never expired by this test.
Parameters
now(?number)
Returns bool
Cookie.to_string()
http.Cookie.to_string()
CookieJar
class http.CookieJar
A client-side cookie store: keeps the cookies a server set, decides which of them a later request is entitled to see, and forgets the ones that have expired.
The matching rules are RFC 6265 §5.4’s: domain-match (an exact host
match, or a suffix match when the cookie carried a Domain),
path-match, and the Secure flag against the request’s scheme. Getting
this wrong in either direction is a real problem - too strict and
sessions break, too loose and a cookie leaks to a host that never should
have seen it.
Constructor
http.CookieJar()
CookieJar.store()
http.CookieJar.store(cookie, host: string, request_path: ?string)
Records a cookie as having been set by host.
A cookie whose Domain is not a suffix of host is rejected outright -
that is a server trying to set a cookie for a domain it does not
control.
Parameters
cookie(Cookie)host(string) — the host of the response that set itrequest_path(?string) — used to derive a default path
Returns — bool: whether the cookie was accepted
CookieJar.cookies_for()
http.CookieJar.cookies_for(host: string, path: string, secure: ?bool) -> dict
The cookies that should be sent with a request to host and path, as
a dictionary of name to value.
Parameters
host(string)path(string)secure(?bool) — whether the request is over HTTPS
Returns dict
CookieJar.all()
http.CookieJar.all() -> list
Every cookie currently held, expired ones included.
Returns list
CookieJar.clear()
http.CookieJar.clear()
Drops every stored cookie.
CookieJar.length()
http.CookieJar.length() -> number
How many cookies are held.
Returns number
2026, Richard Ore and Zuri contributors
http.errors
import http
Everything here is re-exported by
http, soimport httpis enough and the names are called ashttp.*. Importinghttp.errorson its own works too and reaches the same definitions.
Every error the HTTP stack raises, under one root.
HttpError is that root, so a caller that only wants to know whether
the request failed can catch it and ignore the rest. The subclasses
separate the cases worth handling differently: a connection that never
opened, one that timed out, a response too large to accept, a redirect
loop, and a status the caller asked to be treated as an error.
Classes
HttpError
class http.HttpError < Error
Base class for every error the http module raises.
Catching HttpError catches everything this module can throw on its
own; the more specific subclasses below let a caller tell a connection
failure apart from a malformed response without matching on message
text.
- printable — has a
@to_string(), soechoandprint()show something useful
HttpError.to_string()
http.HttpError.to_string()
ProtocolError
class http.ProtocolError < HttpError
Raised when a peer sends something that isn’t a well-formed HTTP message: a broken request line or status line, a header field that doesn’t parse, an impossible framing combination, or a chunked body whose chunk sizes don’t add up.
A server answers these with 400 Bad Request and closes the connection,
since a stream whose framing is in doubt cannot safely be reused.
- printable — has a
@to_string(), soechoandprint()show something useful
ProtocolError.to_string()
http.ProtocolError.to_string()
ConnectionError
class http.ConnectionError < HttpError
Raised when a connection could not be established, or when an established connection failed or was closed before the message being read or written was complete.
- printable — has a
@to_string(), soechoandprint()show something useful
ConnectionError.to_string()
http.ConnectionError.to_string()
TimeoutError
class http.TimeoutError < HttpError
Raised when an operation exceeded its configured deadline: a connect, a read, a write, or the total time budget for a request.
- printable — has a
@to_string(), soechoandprint()show something useful
TimeoutError.to_string()
http.TimeoutError.to_string()
TooLargeError
class http.TooLargeError < HttpError
Raised when a message exceeds one of the configured size limits -
request line, header block, or body. Servers turn this into a 431 or
413 response rather than reading an unbounded amount of
attacker-controlled data into memory.
- printable — has a
@to_string(), soechoandprint()show something useful
TooLargeError.to_string()
http.TooLargeError.to_string()
TooManyRedirectsError
class http.TooManyRedirectsError < HttpError
Raised by the client when a redirect chain exceeds
HttpClient.max_redirects, which usually means the chain is a loop.
- printable — has a
@to_string(), soechoandprint()show something useful
TooManyRedirectsError.to_string()
http.TooManyRedirectsError.to_string()
StatusError
class http.StatusError < HttpError
Raised by HttpResponse.raise_for_status() when the response carries a
4xx or 5xx status. The response itself stays reachable through
response, so the body (which for an API is usually where the actual
explanation lives) isn’t lost.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
response | HttpResponse | The response that carried the failing status. |
Constructor
http.StatusError(message, response)
StatusError.to_string()
http.StatusError.to_string()
UnsupportedProtocolError
class http.UnsupportedProtocolError < HttpError
Raised when a URL names a scheme this module cannot speak, or when a peer insists on a protocol version that was not offered.
- printable — has a
@to_string(), soechoandprint()show something useful
UnsupportedProtocolError.to_string()
http.UnsupportedProtocolError.to_string()
2026, Richard Ore and Zuri contributors
http.files
import http.files
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.files.*needsimport http.files.
Serving files off disk, with the parts that make it correct rather than merely working: conditional requests, entity tags, and byte ranges.
StaticFiles is the handler. ByteRange and the range parser implement
Range/206 Partial Content, which is what lets a browser seek within
a video without downloading it whole.
Functions
parse_range()
http.files.parse_range(header, size: number) -> ?list
Parses a Range header against a representation of size bytes.
Returns a list of ByteRange, an empty list when the header names only
ranges that fall outside the representation (a 416), or nil when the
header is not a byte range at all - which RFC 9110 §14.2 says to ignore
and answer with the whole thing.
Parameters
header(?string)size(number)
Returns ?list
weak_etag()
http.files.weak_etag(stats) -> string
A weak validator derived from a file’s size and modification time.
Weak rather than strong because two writes within the same second are
indistinguishable to it, which is exactly the condition RFC 9110 §8.8.1
reserves the W/ prefix for. It costs one stat, where a strong one
costs a full read of the file.
Parameters
stats(dict) — a file’sstats()dictionary
Returns string
etag_matches()
http.files.etag_matches(header, etag) -> bool
Whether an If-None-Match value matches etag.
Comparison is weak, per RFC 9110 §13.1.2: W/"x" and "x" match each
other, because a conditional GET only needs the two representations to
be equivalent, not byte-identical.
Parameters
header(string)etag(?string)
Returns bool
Classes
ByteRange
class http.ByteRange
One byte range asked for by a Range header, already resolved against
the size of the representation.
Fields
| Field | Type | Description |
|---|---|---|
start | number | The first byte of the range, inclusive. |
last | number | The last byte of the range, inclusive - as Content-Range states it, not as a length. |
Constructor
http.ByteRange(start, last)
ByteRange.length()
http.ByteRange.length() -> number
How many bytes the range covers.
Returns number
ByteRange.to_content_range()
http.ByteRange.to_content_range(total) -> string
The Content-Range field value for this range against a representation
of total bytes.
Parameters
total(number)
Returns string
StaticFiles
class http.StaticFiles
Serves files from a directory, with the conditional-request, range-request and caching behaviour a browser and a CDN both expect.
Every request path is percent-decoded and normalised before it is joined to the root, and the result is checked to still be inside the root afterwards - the decode-then-normalise-then-verify sequence, because doing any two of the three is not enough.
var files = http.StaticFiles('./public', { cache_age: 3600 })
server.handle('GET', '/static/' + '*path', @(request, response) {
files.serve(request, response, request.param('path'))
})
HttpServer.serve_files() wires all of that up for you; this class is
what it uses, and is here for applications that want to serve files from
somewhere the router does not reach.
Fields
| Field | Type | Description |
|---|---|---|
root | string | The directory files are served from, as an absolute path. |
index_files | list | Filenames tried when the request names a directory. |
cache_age | number | max-age, in seconds, for served files. |
etag | bool | Whether to send an ETag. |
precompressed | bool | Whether to answer a request for a file with a .gz or .br sibling by serving that instead, when the client… |
allow_dotfiles | bool | Whether to serve dotfiles. |
fallback | ?string | A file served in place of a missing one, relative to the root. |
Constructor
http.StaticFiles(directory: string, options: ?dict)
Parameters
directory(string)options(?dict) —index_files,cache_age,etag,precompressed,allow_dotfiles,fallback
Raises HttpError if the directory does not exist
StaticFiles.resolve()
http.StaticFiles.resolve(relative_path: string)
Resolves relative_path inside the root, or returns nil when it
escapes, names a dotfile that is not allowed, or does not exist.
Parameters
relative_path(string)
Returns — ?string: an absolute path
StaticFiles.serve()
http.StaticFiles.serve(request, response, relative_path: string)
Serves relative_path from the root into response.
Returns false when there is nothing to serve, leaving the response
untouched so the caller can fall through to its own not-found handling.
Parameters
request(HttpRequest)response(HttpResponse)relative_path(string)
Returns — bool: whether the response was filled in
2026, Richard Ore and Zuri contributors
http.h1
import http
httpexposes this ashttp.h1, soimport httpis enough and the names are called ashttp.h1.*.import http.h1reaches the same definitions directly.
HTTP/1.1 on the wire (RFC 9110 and RFC 9112): reading a request line and its headers, writing a status line and its headers, and the chunked transfer encoding.
This is the layer HttpServer and HttpClient sit on when the
connection is not HTTP/2. DEFAULT_LIMITS is what stops a malformed or
hostile peer from making the parser allocate without bound.
Constants
DEFAULT_LIMITS
http.h1.DEFAULT_LIMITS = {...}
SERVER_NAME
http.h1.SERVER_NAME = 'Zuri'
USER_AGENT
http.h1.USER_AGENT = 'Zuri/1.0'
Functions
parse_version()
http.h1.parse_version(text: string) -> string
Parses HTTP/1.1 into '1.1', rejecting anything that is not a version
this module understands the framing of.
Parameters
text(string)
Returns string
Raises ProtocolError
read_request()
http.h1.read_request(connection, options: ?dict)
Reads one request off a connection: the request line, the header section, and enough framing information to read the body.
The body is not read. request.body_reader is left positioned at its
first byte, so the caller decides whether to materialise it, stream it,
or refuse it - which is the only way to answer a 10 GiB upload with a
413 instead of accepting it first.
Parameters
connection(Connection)options(?dict) — any ofmax_line_size,max_header_size,max_header_count
Returns — ?HttpRequest: nil when the peer closed the connection
cleanly between messages
Raises ProtocolError on a malformed message
Raises TooLargeError if a limit is exceeded
send_continue()
http.h1.send_continue(connection)
Sends a 100 Continue interim response, telling a client that asked
with Expect: 100-continue to go ahead and send its body.
Parameters
connection(Connection)
should_keep_alive()
http.h1.should_keep_alive(request, response) -> bool
Whether the connection should be kept open after this exchange.
HTTP/1.1 keeps connections open unless told otherwise; HTTP/1.0 closes them unless told otherwise. Either side can ask to close, and either side asking is enough.
Parameters
request(HttpRequest)response(HttpResponse)
Returns bool
write_response()
http.h1.write_response(connection, request, response, options: ?dict)
Writes a response to a connection and returns whether the connection is still usable afterwards.
Framing is decided here rather than by the caller, because it has to agree with the status, the method, and the kind of body:
- a
1xx,204or304, and any response toHEAD, sends no body at all no matter what the body holds; - a body of known size gets a
Content-Length; - a streamed body gets chunked encoding on HTTP/1.1, and on HTTP/1.0 - which has no chunked encoding - is delimited by closing the connection.
Parameters
connection(Connection)request(HttpRequest)response(HttpResponse)options(?dict) —server_namefor theServerheader, andkeep_aliveto force the connection decision
Returns — bool: whether the connection may be reused
Raises ConnectionError, TimeoutError
write_request()
http.h1.write_request(connection, request, options: ?dict)
Writes a request to a connection.
Parameters
connection(Connection)request(HttpRequest)options(?dict) —body(bytes), andorigin_form(defaulttrue) to control whether the target is written as a path or as an absolute URL, which is what a request to a forward proxy needs
Raises ConnectionError, TimeoutError
read_response()
http.h1.read_response(connection, request, options: ?dict) -> HttpResponse
Reads a response off a connection.
Interim 1xx responses are consumed and skipped, since they are part of
the exchange rather than its result. A 101 is returned as it is: that
one is the result, and the caller is switching protocols on the
strength of it.
Parameters
connection(Connection)request(HttpRequest) — the request being answered; its method decides the response’s framingoptions(?dict) —max_line_size,max_header_size,max_header_count,max_body_size,decode_content(defaulttrue), andstream(defaultfalse) to leave the body unread
Returns HttpResponse
Raises ProtocolError, TooLargeError, ConnectionError
Classes
ChunkWriter
class http.h1.ChunkWriter
The writer handed to a streaming response body.
write() sends data as it is produced, framed as chunked transfer
encoding when that is how the response was framed, and raw otherwise.
flush() pushes what has been written all the way to the socket, which
is what makes a progress feed or a server-sent-event stream actually
arrive rather than sit in a buffer.
Constructor
http.h1.ChunkWriter(connection, chunked)
Parameters
connection(Connection)chunked(bool)
ChunkWriter.write()
http.h1.ChunkWriter.write(data)
Writes part of the body.
A zero-length write is dropped rather than sent, since a zero-length chunk is the end-of-body marker and sending one early would truncate the response.
Parameters
data(bytes|string)
ChunkWriter.flush()
http.h1.ChunkWriter.flush()
Pushes everything written so far out to the socket.
ChunkWriter.finish()
http.h1.ChunkWriter.finish(trailers)
Ends the body, writing the terminating chunk when the response is chunked. Called for you when the streaming handler returns.
Parameters
trailers(?Headers) — trailer fields to send after the last chunk; only meaningful for a chunked body, and only ever read by a client that announcedTE: trailers
ChunkWriter.abort()
http.h1.ChunkWriter.abort()
2026, Richard Ore and Zuri contributors
http.h2
import http
httpexposes this ashttp.h2, soimport httpis enough and the names are called ashttp.h2.*.import http.h2reaches the same definitions directly.
HTTP/2 (RFC 9113) and the header compression it uses (RFC 7541).
HttpServer speaks this automatically: over TLS when ALPN negotiates
h2, and over cleartext when a client opens with the HTTP/2 connection
preface. HttpClient uses it when a server negotiates h2 for an HTTPS
connection.
The pieces are here for anything that needs them directly - a gateway, a test harness, or a protocol that borrows HPACK.
Submodules
| Module | Reached as | Summary |
|---|---|---|
http.h2.connection | http.h2.connection.* | One HTTP/2 connection and the streams multiplexed over it. |
http.h2.frames | http.h2.frames.* | The HTTP/2 frame layer (RFC 9113 §4 and §6): a nine-octet header - a 24-bit length, a type, a flags byte, and… |
http.h2.hpack | http.h2.hpack.* | HPACK, the header compression HTTP/2 uses (RFC 7541). |
http.h2.huffman | http.h2.huffman.* | The static Huffman code HPACK uses to compress header field names and values, from RFC 7541 Appendix B. |
2026, Richard Ore and Zuri contributors
http.h2.connection
import http.h2.connection
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.h2.connection.*needsimport http.h2.connection.
One HTTP/2 connection and the streams multiplexed over it.
Http2Connection owns the connection-level state: the settings
exchange, flow-control windows, and the HPACK contexts shared by every
stream. Http2Stream is one request/response pair within it, and
Http2Writer serialises frames so that concurrent streams cannot
interleave mid-frame.
Classes
Http2Stream
class http.h2.Http2Stream
One HTTP/2 stream: a request and its response, multiplexed with others over a single connection.
Fields
| Field | Type | Description |
|---|---|---|
id | int | The stream identifier. |
fields | list | The decoded header fields, in the order they arrived. |
data | bytes | The request or response body received so far. |
ended | bool | Whether the peer has finished sending on this stream. |
headers_done | bool | Whether the header block is complete - that is, whether an END_HEADERS flag has been seen. |
closed | bool | Whether this endpoint has finished sending on this stream. |
send_window | number | How many bytes may still be sent on this stream before the peer has to grant more. |
consumed | number | How much of this stream’s receive window has been consumed since the last WINDOW_UPDATE. |
trailers | list | The trailer fields, if any arrived after the body. |
fragment |
Constructor
http.h2.Http2Stream(id, send_window)
Http2Connection
class http.h2.Http2Connection
An HTTP/2 connection, in either role.
On the server side, serve() reads the preface, exchanges settings, and
then runs the frame loop, handing each completed request to the
HttpServer it was given and writing the response back. On the client
side, start_client() performs the same handshake and
send_request()/await_response() carry one exchange.
Streams are multiplexed on the wire but handled in the order they complete: this is one thread reading one socket, so a request that arrives while another is being handled is read, buffered, and answered next rather than in parallel. That is a throughput property, not a correctness one - the framing, flow control and header state are all maintained exactly as the protocol requires.
Fields
| Field | Type | Description |
|---|---|---|
is_server | bool | Whether this endpoint is the server. |
max_body_size | number | The largest request body accepted on one stream, in bytes. |
Constructor
http.h2.Http2Connection(connection, is_server: bool)
Parameters
connection(Connection) — the transport, already connected and (forh2) already through its TLS handshakeis_server(bool)
Http2Connection.start_server()
http.h2.Http2Connection.start_server(already_read)
Reads the client connection preface and sends this endpoint’s SETTINGS. Server side.
Parameters
already_read(?bytes) — preface bytes a caller consumed while working out which protocol this was
Raises ProtocolError if the preface is not what HTTP/2 requires
Http2Connection.start_client()
http.h2.Http2Connection.start_client()
Sends the client connection preface and this endpoint’s SETTINGS. Client side.
Http2Connection.serve()
http.h2.Http2Connection.serve(server)
Runs the connection until the peer goes away, handing every completed
request to server.
Parameters
server(HttpServer)
Http2Connection.send_data()
http.h2.Http2Connection.send_data(stream, data, end_stream)
Sends data on stream as DATA frames, respecting both the stream’s
and the connection’s send windows and the peer’s maximum frame size.
When a window is exhausted this reads frames until the peer grants more, which is what makes flow control actually work rather than simply be accounted for.
Parameters
stream(Http2Stream)data(bytes)end_stream(bool)
Http2Connection.send_request()
http.h2.Http2Connection.send_request(request, payload) -> Http2Stream
Opens a stream and sends request on it. Client side.
Parameters
request(HttpRequest)payload(?bytes) — the request body
Returns Http2Stream
Http2Connection.await_response()
http.h2.Http2Connection.await_response(stream, request) -> HttpResponse
Reads frames until stream is finished and returns the response it
carried. Client side.
Parameters
stream(Http2Stream)request(HttpRequest)
Returns HttpResponse
Raises ProtocolError if the response is malformed
Raises ConnectionError if the connection ends first
Http2Connection.is_usable()
http.h2.Http2Connection.is_usable() -> bool
Whether the connection is still usable for another request.
Returns bool
Http2Connection.close()
http.h2.Http2Connection.close(error_code)
Sends GOAWAY and closes the transport.
Parameters
error_code(?int) — defaults toNO_ERROR
Http2Writer
class http.h2.Http2Writer
The writer a streaming response body gets on an HTTP/2 connection. Same shape as the HTTP/1.1 one, so a handler never has to know which version it is answering.
Constructor
http.h2.Http2Writer(connection, stream)
Http2Writer.write()
http.h2.Http2Writer.write(data)
Sends part of the body.
Parameters
data(bytes|string)
Http2Writer.flush()
http.h2.Http2Writer.flush()
Pushes what has been written out to the socket. DATA frames are already flushed as they go, so this is here for interface parity with the HTTP/1.1 writer.
Http2Writer.finish()
http.h2.Http2Writer.finish()
Ends the body with an empty END_STREAM frame.
2026, Richard Ore and Zuri contributors
http.h2.frames
import http
httpexposes this ashttp.h2.frames, soimport httpis enough and the names are called ashttp.h2.frames.*.import http.h2.framesreaches the same definitions directly.
The HTTP/2 frame layer (RFC 9113 §4 and §6): a nine-octet header - a 24-bit length, a type, a flags byte, and a 31-bit stream identifier - followed by a payload.
Constants
PREFACE
http.h2.frames.PREFACE: string = 'PRI * HTTP/2.0
SM
'
The connection preface every HTTP/2 client sends before anything else. A
server that reads these exact bytes knows it is speaking HTTP/2 and not
an HTTP/1.1 request that happens to start with PRI.
DATA
http.h2.frames.DATA = 0
DATA frame. @type int
HEADERS
http.h2.frames.HEADERS = 1
HEADERS frame. @type int
PRIORITY
http.h2.frames.PRIORITY = 2
PRIORITY frame; deprecated by RFC 9113 and ignored here. @type int
RST_STREAM
http.h2.frames.RST_STREAM = 3
RST_STREAM frame. @type int
SETTINGS
http.h2.frames.SETTINGS = 4
SETTINGS frame. @type int
PUSH_PROMISE
http.h2.frames.PUSH_PROMISE = 5
PUSH_PROMISE frame. @type int
PING
http.h2.frames.PING = 6
PING frame. @type int
GOAWAY
http.h2.frames.GOAWAY = 7
GOAWAY frame. @type int
WINDOW_UPDATE
http.h2.frames.WINDOW_UPDATE = 8
WINDOW_UPDATE frame. @type int
CONTINUATION
http.h2.frames.CONTINUATION = 9
CONTINUATION frame. @type int
FLAG_END_STREAM
http.h2.frames.FLAG_END_STREAM = 1
END_STREAM flag, on DATA and HEADERS. @type int
FLAG_ACK
http.h2.frames.FLAG_ACK = 1
ACK flag, on SETTINGS and PING. @type int
FLAG_END_HEADERS
http.h2.frames.FLAG_END_HEADERS = 4
END_HEADERS flag, on HEADERS, PUSH_PROMISE and CONTINUATION. @type int
FLAG_PADDED
http.h2.frames.FLAG_PADDED = 8
PADDED flag, on DATA, HEADERS and PUSH_PROMISE. @type int
FLAG_PRIORITY
http.h2.frames.FLAG_PRIORITY = 32
PRIORITY flag, on HEADERS. @type int
SETTINGS_HEADER_TABLE_SIZE
http.h2.frames.SETTINGS_HEADER_TABLE_SIZE = 1
SETTINGS_HEADER_TABLE_SIZE. @type int
SETTINGS_ENABLE_PUSH
http.h2.frames.SETTINGS_ENABLE_PUSH = 2
SETTINGS_ENABLE_PUSH. @type int
SETTINGS_MAX_CONCURRENT_STREAMS
http.h2.frames.SETTINGS_MAX_CONCURRENT_STREAMS = 3
SETTINGS_MAX_CONCURRENT_STREAMS. @type int
SETTINGS_INITIAL_WINDOW_SIZE
http.h2.frames.SETTINGS_INITIAL_WINDOW_SIZE = 4
SETTINGS_INITIAL_WINDOW_SIZE. @type int
SETTINGS_MAX_FRAME_SIZE
http.h2.frames.SETTINGS_MAX_FRAME_SIZE = 5
SETTINGS_MAX_FRAME_SIZE. @type int
SETTINGS_MAX_HEADER_LIST_SIZE
http.h2.frames.SETTINGS_MAX_HEADER_LIST_SIZE = 6
SETTINGS_MAX_HEADER_LIST_SIZE. @type int
NO_ERROR
http.h2.frames.NO_ERROR = 0
NO_ERROR. @type int
PROTOCOL_ERROR
http.h2.frames.PROTOCOL_ERROR = 1
PROTOCOL_ERROR. @type int
INTERNAL_ERROR
http.h2.frames.INTERNAL_ERROR = 2
INTERNAL_ERROR. @type int
FLOW_CONTROL_ERROR
http.h2.frames.FLOW_CONTROL_ERROR = 3
FLOW_CONTROL_ERROR. @type int
SETTINGS_TIMEOUT
http.h2.frames.SETTINGS_TIMEOUT = 4
SETTINGS_TIMEOUT. @type int
STREAM_CLOSED
http.h2.frames.STREAM_CLOSED = 5
STREAM_CLOSED. @type int
FRAME_SIZE_ERROR
http.h2.frames.FRAME_SIZE_ERROR = 6
FRAME_SIZE_ERROR. @type int
REFUSED_STREAM
http.h2.frames.REFUSED_STREAM = 7
REFUSED_STREAM. @type int
CANCEL
http.h2.frames.CANCEL = 8
CANCEL. @type int
COMPRESSION_ERROR
http.h2.frames.COMPRESSION_ERROR = 9
COMPRESSION_ERROR. @type int
CONNECT_ERROR
http.h2.frames.CONNECT_ERROR = 10
CONNECT_ERROR. @type int
ENHANCE_YOUR_CALM
http.h2.frames.ENHANCE_YOUR_CALM = 11
ENHANCE_YOUR_CALM. @type int
INADEQUATE_SECURITY
http.h2.frames.INADEQUATE_SECURITY = 12
INADEQUATE_SECURITY. @type int
HTTP_1_1_REQUIRED
http.h2.frames.HTTP_1_1_REQUIRED = 13
HTTP_1_1_REQUIRED. @type int
Functions
read_u32()
http.h2.frames.read_u32(data, offset: number) -> number
Reads a 32-bit big-endian integer from data at offset.
Parameters
data(bytes)offset(number)
Returns number
write_u32()
http.h2.frames.write_u32(out, value: number)
Appends a 32-bit big-endian integer to out.
Parameters
out(bytes)value(number)
read_u24()
http.h2.frames.read_u24(data, offset: number) -> number
Reads a 24-bit big-endian integer from data at offset.
Parameters
data(bytes)offset(number)
Returns number
read_frame()
http.h2.frames.read_frame(connection, max_frame_size: number) -> Frame
Reads one frame off a connection.
Padding is stripped here, so a caller never has to think about it, and a pad length that does not fit inside the frame is rejected - that mismatch is a connection error, not a frame to salvage.
Parameters
connection(Connection)max_frame_size(number) — refuse anything larger
Returns Frame
Raises ProtocolError on a malformed or over-large frame
write_frame()
http.h2.frames.write_frame(connection, type: int, flags: int, stream_id: int, payload)
Writes one frame to a connection.
Parameters
connection(Connection)type(int)flags(int)stream_id(int)payload(?bytes)
encode_settings()
http.h2.frames.encode_settings(values: dict) -> bytes
Builds a SETTINGS payload from a dictionary of identifier to value.
Parameters
values(dict)
Returns bytes
decode_settings()
http.h2.frames.decode_settings(payload) -> dict
Parses a SETTINGS payload into a dictionary.
Unknown identifiers are kept rather than dropped - an endpoint must ignore settings it does not understand, and keeping them makes that the caller’s decision rather than a silent one here.
Parameters
payload(bytes)
Returns dict
Raises ProtocolError if the payload is not a whole number of
six-octet entries
encode_goaway()
http.h2.frames.encode_goaway(last_stream_id: int, error_code: int, debug_message: ?string) -> bytes
Builds a GOAWAY payload.
Parameters
last_stream_id(int) — the highest stream this endpoint acted onerror_code(int)debug_message(?string) — for humans reading a packet capture; never interpreted by the peer
Returns bytes
encode_error()
http.h2.frames.encode_error(error_code: int) -> bytes
Builds an RST_STREAM payload.
Parameters
error_code(int)
Returns bytes
Classes
Frame
class http.h2.frames.Frame
One frame, as read off the wire.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
type | int | The frame type - one of the constants above. |
flags | int | The flags byte. |
stream_id | int | The stream this frame belongs to; 0 for connection-level frames. |
payload | bytes | The payload, with any padding already removed. |
Constructor
http.h2.frames.Frame(type, flags, stream_id, payload)
Frame.has_flag()
http.h2.frames.Frame.has_flag(flag: int) -> bool
Whether flag is set.
Parameters
flag(int)
Returns bool
Frame.to_string()
http.h2.frames.Frame.to_string()
2026, Richard Ore and Zuri contributors
http.h2.hpack
import http
httpexposes this ashttp.h2.hpack, soimport httpis enough and the names are called ashttp.h2.hpack.*.import http.h2.hpackreaches the same definitions directly.
HPACK, the header compression HTTP/2 uses (RFC 7541).
A header block is a sequence of representations, each of which either names an entry of a table both endpoints maintain, or spells a field out. The static table below is fixed by the RFC; the dynamic table is built as a connection runs, which is what makes the decoder stateful - a block cannot be decoded without every block that came before it on the same connection.
The encoder here uses the static table where it can and literals everywhere else, and never adds to its own dynamic table. That gives up some compression and buys a great deal: an encoder with no dynamic table cannot desynchronise from a peer’s decoder, and it cannot leak one request’s secrets into another’s compression ratio, which is the whole of the CRIME family of attacks.
Functions
encode_integer()
http.h2.hpack.encode_integer(out, value: number, prefix_bits: number, first: number)
Encodes an integer with an prefix_bits-wide prefix, per RFC 7541 §5.1.
first is whatever flag bits share that first byte.
Parameters
out(bytes)value(number)prefix_bits(number)first(number) — the flag bits, already shifted into place
decode_integer()
http.h2.hpack.decode_integer(data, offset: number, prefix_bits: number)
Decodes an integer with an prefix_bits-wide prefix, starting at
offset.
Parameters
data(bytes)offset(number)prefix_bits(number)
Returns — list: [value, next offset]
Raises ProtocolError if the encoding runs off the end or is
absurdly long
encode_string()
http.h2.hpack.encode_string(out, text: string)
Encodes a string literal, Huffman-coding it when that comes out shorter.
Parameters
out(bytes)text(string)
decode_string()
http.h2.hpack.decode_string(data, offset: number, max_length: number)
Decodes a string literal starting at offset.
Parameters
data(bytes)offset(number)max_length(number) — refuse a literal longer than this
Returns — list: [text, next offset]
Raises ProtocolError on a truncated or over-long literal
Classes
DynamicTable
class http.h2.hpack.DynamicTable
The dynamic table half of an HPACK context: the most recently inserted fields, evicted from the far end when the table would exceed the size its owner has been told to keep.
Entries are indexed from the newest, and continue the numbering where the static table stops, so index 62 is always the most recently added entry.
Constructor
http.h2.hpack.DynamicTable(capacity)
Parameters
capacity(?number) — the maximum table size in bytes; defaults to HPACK’s own default of 4096
DynamicTable.add()
http.h2.hpack.DynamicTable.add(name: string, value: string)
Adds a field, evicting from the oldest end until it fits.
An entry larger than the whole table is not an error: RFC 7541 §4.4 says to empty the table and add nothing, which is what happens here.
Parameters
name(string)value(string)
DynamicTable.get()
http.h2.hpack.DynamicTable.get(index: number)
The entry at HPACK index index, which counts the static table first.
Parameters
index(number)
Returns — list: [name, value]
Raises ProtocolError if the index names nothing
DynamicTable.set_capacity()
http.h2.hpack.DynamicTable.set_capacity(capacity: number)
Changes the table’s maximum size, evicting whatever no longer fits.
Parameters
capacity(number)
DynamicTable.capacity()
http.h2.hpack.DynamicTable.capacity() -> number
The table’s current maximum size in bytes.
Returns number
DynamicTable.size()
http.h2.hpack.DynamicTable.size() -> number
How many bytes the entries currently occupy, by HPACK’s accounting (name plus value plus 32 per entry).
Returns number
DynamicTable.length()
http.h2.hpack.DynamicTable.length() -> number
How many entries the table holds.
Returns number
Decoder
class http.h2.hpack.Decoder
Decodes header blocks for one direction of one connection.
A decoder is stateful and order-dependent: it must see every header block on its connection, in order, or its dynamic table drifts out of step with the peer’s encoder and every block after that decodes to nonsense.
Fields
| Field | Type | Description |
|---|---|---|
table | ||
max_string_length | number | The largest single string literal accepted. |
max_header_list_size | number | The largest decoded header list accepted, by HPACK’s own size accounting. |
Constructor
http.h2.hpack.Decoder(capacity)
Parameters
capacity(?number) — the initial dynamic table size
Decoder.decode()
http.h2.hpack.Decoder.decode(block) -> list
Decodes a complete header block into a list of [name, value] pairs, in
the order they appeared - which HTTP/2 depends on, as repeated fields
keep their order and Set-Cookie may appear many times.
Parameters
block(bytes)
Returns list
Raises ProtocolError on a malformed block or one that exceeds a
limit
Encoder
class http.h2.hpack.Encoder
Encodes header blocks for one direction of one connection.
Fields are matched against the static table, and anything not found there is written out as a literal without indexing - see the module note above for why the dynamic table is left empty.
Constructor
http.h2.hpack.Encoder(capacity)
Encoder.set_capacity()
http.h2.hpack.Encoder.set_capacity(capacity: number)
Notes the maximum dynamic table size the peer will accept.
Parameters
capacity(number)
Encoder.encode()
http.h2.hpack.Encoder.encode(fields: list) -> bytes
Encodes a list of [name, value] pairs into a header block.
Names must already be lowercase; HTTP/2 has no other kind.
Parameters
fields(list)
Returns bytes
2026, Richard Ore and Zuri contributors
http.h2.huffman
import http
httpexposes this ashttp.h2.huffman, soimport httpis enough and the names are called ashttp.h2.huffman.*.import http.h2.huffmanreaches the same definitions directly.
The static Huffman code HPACK uses to compress header field names and values, from RFC 7541 Appendix B.
Only the code lengths are written out below. The code itself is
canonical - codes are assigned in increasing symbol order within each
length - so every one of the 257 codes can be derived from its length
alone, which is both far less to state and far less to get wrong.
_build() does the derivation at load time and also checks the Kraft
sum, which comes to exactly one for a complete prefix code and to
something else the moment a single length is wrong.
Constants
EOS
http.h2.huffman.EOS: int = 256
The number of the symbol HPACK uses to pad the final byte of a Huffman-encoded string, and which must never appear as a decoded value.
Functions
encoded_length()
http.h2.huffman.encoded_length(data) -> number
The number of bytes data would occupy once Huffman-encoded.
The encoder uses this to decide whether encoding is worth it at all: HPACK lets a string go out as-is, and for a string of mostly high-entropy bytes the encoded form is the longer one.
Parameters
data(bytes)
Returns number
encode()
http.h2.huffman.encode(data) -> bytes
Huffman-encodes data, padding the final byte with the leading bits of
the EOS code (which are all ones) as RFC 7541 §5.2 requires.
Parameters
data(bytes)
Returns bytes
decode()
http.h2.huffman.decode(data, start, end) -> bytes
Decodes a Huffman-encoded string.
The padding at the end must be the leading bits of EOS and no longer than seven bits; anything else is a connection-level error per RFC 7541 §5.2, since it is the shape a smuggled second header takes.
Parameters
data(bytes)start(?number)end(?number)
Returns bytes
Raises ProtocolError on invalid padding or an encoded EOS
http.headers
import http.headers
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.headers.*needsimport http.headers.
Headers: a case-insensitive, order-preserving multimap, because HTTP
header names do not compare case-sensitively and some headers may
legitimately appear more than once.
The validation functions enforce what a name and a value may contain, which is what keeps a header value carrying a newline from splitting one response into two.
Functions
is_valid_name()
http.headers.is_valid_name(name) -> bool
Whether name is a syntactically valid HTTP field name, i.e. a
non-empty RFC 9110 token.
Parameters
name(string)
Returns bool
is_valid_value()
http.headers.is_valid_value(value) -> bool
Whether value is a legal HTTP field value.
The rule that actually matters here is that a value may not contain CR,
LF, or NUL. Letting any of those through is response splitting: a value
carrying \r\n ends the field early and lets whoever supplied it inject
headers, or a whole second response, into the stream.
Leading and trailing whitespace is not part of the value and is stripped before the check rather than rejected.
Parameters
value(string)
Returns bool
canonical_name()
http.headers.canonical_name(name: string) -> string
The conventional spelling of a field name, e.g. 'content-type' becomes
'Content-Type' and 'etag' becomes 'ETag'.
Field names are case-insensitive on the wire, so this is purely cosmetic
- but it is the casing every other HTTP implementation emits, and some badly written clients do compare case-sensitively.
Parameters
name(string)
Returns string
parse()
http.headers.parse(block: string, strict: ?bool) -> Headers
Parses a raw header block - everything between a start line and the
blank line that ends the head - into a Headers.
Obsolete line folding (a continuation line starting with a space) is rejected outright rather than unfolded: RFC 9112 §5.2 deprecated it, and treating it as data is the safer reading given how differently intermediaries handle it.
Parameters
block(string) — the header lines, without the trailing blank linestrict(?bool) — whentrue(the default), a field appearing more than once that is defined to appear at most once raises instead of being folded
Returns Headers
Raises ProtocolError on a malformed field, a folded line, or a
duplicated singleton field
is_never_folded()
http.headers.is_never_folded(name: string) -> bool
Whether a field with this name must be repeated rather than folded into one comma-separated value when written out.
Parameters
name(string)
Returns bool
Classes
Headers
class http.Headers
An ordered, case-insensitive, multi-value collection of HTTP header fields.
Field names are matched without regard to case, so
headers.get('content-type') and headers.get('Content-Type') are the
same lookup, while the spelling a field was first added under is what
gets written back out. Insertion order is preserved.
A field may legally appear more than once. set() replaces every
occurrence, add() appends another one, get() returns the first, and
get_all() returns them all:
var h = http.Headers()
h.set('Accept', 'text/html')
h.add('Accept', 'application/json')
h.get('accept') # 'text/html'
h.get_all('accept') # ['text/html', 'application/json']
Every name and value is validated on the way in. A name that isn’t a token, or a value carrying CR, LF or NUL, raises rather than being quietly sanitised, because a value that can inject a newline is a response-splitting bug wherever it eventually lands.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode()
Constructor
http.Headers(fields)
Builds a Headers collection, optionally seeded from a dictionary of
name -> value (or name -> [value, ...]) pairs.
Parameters
fields(?dict)
Headers.get()
http.Headers.get(name: string, fallback) -> any
The first value recorded for name, or fallback (default nil) when
the field is absent.
Parameters
name(string)fallback(?any)
Returns any
Headers.get_all()
http.Headers.get_all(name: string) -> list
Every value recorded for name, in the order they were added, or an
empty list when the field is absent.
Parameters
name(string)
Returns list
Headers.get_joined()
http.Headers.get_joined(name: string, fallback) -> any
All values for name joined with ', ' - the form RFC 9110 §5.3
defines as equivalent to repeating the field - or fallback when the
field is absent.
Never use this for Set-Cookie; commas are legal inside a cookie value
and joining them corrupts the result. get_all() is the right call
there.
Parameters
name(string)fallback(?any)
Returns any
Headers.set()
http.Headers.set(name: string, value)
Replaces every value of name with value. The name keeps whatever
spelling it is given here.
Parameters
name(string)value(string|number|bool)
Returns — Headers: this same instance, for chaining
Raises ProtocolError if the name is not a token or the value
contains CR, LF, or NUL
Headers.add()
http.Headers.add(name: string, value)
Records another value for name, keeping any already there.
Parameters
name(string)value(string|number|bool)
Returns — Headers: this same instance, for chaining
Raises ProtocolError if the name is not a token or the value
contains CR, LF, or NUL
Headers.set_default()
http.Headers.set_default(name: string, value)
Sets name to value only if the field is not already present. Used
throughout this module for defaults a caller is free to override -
Date, Server, User-Agent and friends.
Parameters
name(string)value(string|number|bool)
Returns — Headers: this same instance, for chaining
Headers.remove()
http.Headers.remove(name: string)
Removes every value of name. Removing an absent field does nothing
rather than failing.
Parameters
name(string)
Returns — Headers: this same instance, for chaining
Headers.contains()
http.Headers.contains(name: string) -> bool
Whether name is present at all.
Parameters
name(string)
Returns bool
Headers.contains_token()
http.Headers.contains_token(name: string, token: string) -> bool
Whether name is present and one of its comma-separated tokens equals
token, compared case-insensitively.
This is the correct way to ask about the list-valued fields that drive
protocol decisions - Connection: keep-alive, Upgrade,
Transfer-Encoding: gzip, chunked - where a naive substring test would
happily match keep-alive-ish and a naive equality test would miss the
second token entirely.
Parameters
name(string)token(string)
Returns bool
Headers.tokens()
http.Headers.tokens(name: string) -> list
Every comma-separated token across every occurrence of name,
lowercased and trimmed, in order. Empty elements are dropped, as RFC
9110 §5.6.1 permits.
Parameters
name(string)
Returns list
Headers.extend()
http.Headers.extend(other)
Copies every field from other into this collection, replacing fields
of the same name. other may be another Headers or a plain dictionary
whose values are strings or lists of strings.
Parameters
other(Headers|dict)
Returns — Headers: this same instance, for chaining
Headers.each()
http.Headers.each(callback)
Calls callback(name, value) once per field occurrence, in insertion
order - so a field present three times triggers three calls, which is
exactly what writing the block back out needs.
Parameters
callback(function(2))
Headers.names()
http.Headers.names() -> list
The field names present, in insertion order and in the spelling they were added under.
Returns list
Headers.length()
http.Headers.length() -> number
The number of distinct field names present. A field repeated three times counts once.
Returns number
Headers.is_empty()
http.Headers.is_empty() -> bool
Whether there are no fields at all.
Returns bool
Headers.size()
http.Headers.size() -> number
The number of bytes this block would occupy once serialised, counting
the ': ' and CRLF around every value. Servers use this to enforce a
header-size limit as fields arrive.
Returns number
Headers.clear()
http.Headers.clear()
Drops every field. Cheaper than building a new instance when a connection is being reused for the next message.
Returns — Headers: this same instance, for chaining
Headers.clone()
http.Headers.clone() -> Headers
An independent copy. Mutating the copy never affects the original.
Returns Headers
Headers.canonicalize()
http.Headers.canonicalize()
Rewrites every field name to its conventional casing, e.g.
content-type to Content-Type. Purely cosmetic; names remain
case-insensitive either way.
Returns — Headers: this same instance, for chaining
Headers.strip_hop_by_hop()
http.Headers.strip_hop_by_hop()
Removes the hop-by-hop fields listed in RFC 9110 §7.6.1, which describe
one connection rather than the message itself and must never be
forwarded on to another connection. The set includes anything named by
this message’s own Connection field, which is how an endpoint declares
an extra hop-by-hop field.
A reverse proxy that forgets this is how a client-supplied
Transfer-Encoding reaches an upstream that frames it differently - the
classic smuggling setup.
Returns — Headers: this same instance, for chaining
Headers.to_wire()
http.Headers.to_wire() -> string
Serialises the block in wire format: Name: value\r\n per occurrence,
with no terminating blank line (the caller decides where the block ends,
since a trailer section ends differently to a head).
Returns string
Headers.to_dict()
http.Headers.to_dict() -> dict
The block as a plain dictionary of name -> value, folding repeated
fields into a list. Handy for logging and for handing headers to code
that has no reason to know about this class.
Returns dict
Headers.to_string()
http.Headers.to_string()
2026, Richard Ore and Zuri contributors
http.middleware
import http
httpexposes this ashttp.middleware, soimport httpis enough and the names are called ashttp.middleware.*.import http.middlewarereaches the same definitions directly.
The middleware every public HTTP service ends up needing: CORS, access logging, the security headers a browser acts on, authentication, and a request rate limit.
Each function here returns a middleware - a function of (request, response, next)
- ready to hand to
HttpServer.use():
import http
import http.middleware
var server = http.server(3000)
server.use(middleware.logger())
server.use(middleware.security_headers())
server.use(middleware.cors({ origins: ['https://app.example.com'] }))
Order matters: middleware run outermost first, so a logger added first sees the final status of everything added after it.
Functions
cors()
http.middleware.cors(options: ?dict) -> function(3)
Answers CORS preflights and adds the cross-origin headers a browser needs before it will let script read a response from another origin.
origins is the list of origins allowed, matched exactly. The literal
'*' allows any origin, but only for requests that carry no
credentials: a wildcard together with Access-Control-Allow-Credentials
is rejected by every browser, so setting credentials reflects the
request’s own origin instead - which means the origins list becomes
the only thing standing between an attacker’s page and an authenticated
response, and it needs to be an explicit list rather than a wildcard.
Parameters
options(?dict) —origins(default['*']),methods,headers(allowed request headers; defaults to reflecting what was asked for),expose(response headers script may read),credentials(defaultfalse),max_age(seconds a preflight may be cached, default 86400)
Returns function(3)
logger()
http.middleware.logger(options: ?dict) -> function(3)
Writes one line per request once the response is finished.
The default format is the Common Log Format extended with the response
time, which every log analyser already understands. Pass format to
build the line yourself, or sink to send it somewhere other than
standard output.
Parameters
options(?dict) —sink(a function taking the line),format(a function taking(request, response, milliseconds)and returning the line),trust_proxy(whether to log the forwarded client address rather than the peer)
Returns function(3)
security_headers()
http.middleware.security_headers(options: ?dict) -> function(3)
Adds the response headers a browser acts on to harden a page.
Every one of them is opt-out, because the right value depends on the application:
| Header | Default | What it does |
|---|---|---|
X-Content-Type-Options | nosniff | stops the browser second-guessing a declared content type |
X-Frame-Options | DENY | refuses to be framed, which is what clickjacking needs |
Referrer-Policy | strict-origin-when-cross-origin | keeps paths and queries out of outbound referrers |
Strict-Transport-Security | one year | only sent over HTTPS, where it is meaningful |
Content-Security-Policy | not set | too application-specific to guess at |
Parameters
options(?dict) — any of the header values above by lowercase name (content_security_policy,frame_options,referrer_policy,hsts_max_age,hsts_subdomains,content_type_options), eachnilto omit
Returns function(3)
basic_auth()
http.middleware.basic_auth(verify, realm: ?string) -> function(3)
Requires HTTP Basic authentication.
verify is called with the username and password and returns whether
they are acceptable; it should compare secrets with
http.util.secure_equals() rather than ==, so that a wrong guess and
a nearly-right one take the same time.
A nil verifier disables the middleware: it passes every request
straight through rather than refusing one. That makes blanking the
verifier a way to switch authentication off without unpicking the chain
around it.
Parameters
verify(?function(2)) —nilto disablerealm(?string) — the realm named in the challenge; defaults to'Restricted'
Returns function(3)
bearer_auth()
http.middleware.bearer_auth(verify, realm: ?string) -> function(3)
Requires a bearer token.
A nil verifier disables the middleware, as with basic_auth().
Parameters
verify(?function(1)) — called with the token; returns whether it is acceptable, or a value to attach torequest.contextas'user'.nilto disablerealm(?string)
Returns function(3)
parse_basic()
http.middleware.parse_basic(header: string)
Parses an HTTP Basic Authorization header into a username and
password.
Parameters
header(string)
Returns — ?list: [username, password], or nil if the header is
not a well-formed Basic credential
parse_bearer()
http.middleware.parse_bearer(header: string) -> ?string
Parses a Bearer Authorization header into its token.
Parameters
header(string)
Returns ?string
jwt_auth()
http.middleware.jwt_auth(verifier, options: ?dict) -> function(3)
Requires a valid JSON Web Token, verified by the jwt module.
verifier is either a jwt.Verifier, or any function taking the token
and returning its claims:
import http.middleware
import jwt
server.use(middleware.jwt_auth(
jwt.Verifier(secret, { algorithms: ['HS256'], audience: 'api' })
))
The function form is what covers a key set resolved by kid, or
anything else the jwt module can do that a fixed verifier cannot:
server.use(middleware.jwt_auth(@(token) {
return jwt.verify_with_jwks(token, keys, { audience: 'api' })
}))
On success the claims land on request.context['claims'], and the sub
claim - the usual place an issuer puts the account the token speaks for
- on
request.context['user'].
Failures follow RFC 6750 §3: a request with no token is answered 401
with a bare Bearer challenge, one whose token does not verify is
answered 401 with error="invalid_token", and one whose token is
valid but lacks a required scope is answered 403 with
error="insufficient_scope".
A nil verifier disables the middleware: every request passes straight
through, unauthenticated. Nothing here raises, so registering it never
needs a catch block around it.
Parameters
verifier(Verifier|function(1)|nil) —nilto disableoptions(?dict) —realm(default'api'),optional(attach the claims when a valid token is present but do not refuse a request without one), andscopes(a list every token must carry)
Returns function(3)
Note: Like
HttpRequest.validate(), this deliberately does not import thejwtmodule. The verifier is built by the caller, which keeps the token format entirely the application’s business and means a server that authenticates nothing never pays to load it.
rate_limit()
http.middleware.rate_limit(options: ?dict) -> function(3)
Limits how many requests one client may make in a window of time.
The counter lives in memory, so it is per worker: with workers
isolates the effective limit is limit times workers. That is a
deliberate trade - a shared counter would need shared state, and this is
meant to blunt a runaway client rather than to meter billing.
Parameters
options(?dict) —limit(requests per window, default 60),window(seconds, default 60),key(a function of the request returning the bucket key; defaults to the client address),trust_proxy
Returns function(3)
request_id()
http.middleware.request_id(header: ?string) -> function(3)
Attaches a unique identifier to every request, echoing back one the client supplied so a trace can be followed across services.
The identifier lands on request.context['request_id'] and in the
response’s X-Request-Id.
Parameters
header(?string) — the header to read and write; defaults to'X-Request-Id'
Returns function(3)
force_https()
http.middleware.force_https(options: ?dict) -> function(3)
Sends every request that arrived over cleartext to the same URL over HTTPS.
Parameters
options(?dict) —status(default308, which preserves the method),port(the HTTPS port, when it is not 443)
Returns function(3)
etag()
http.middleware.etag(min_size: ?number) -> function(3)
Computes a weak ETag over a finished response body and answers 304 Not Modified
when the client already has that version.
A handler that already set its own ETag is left alone - it knows
something about the resource that hashing the bytes does not.
Parameters
min_size(?number) — bodies smaller than this are not tagged; defaults to 128 bytes
Returns function(3)
2026, Richard Ore and Zuri contributors
http.multipart
import http.multipart
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.multipart.*needsimport http.multipart.
multipart/form-data, in both directions.
MultipartData and UploadedFile are what a parsed upload arrives as;
MultipartBuilder constructs one for sending. Parsing streams rather
than buffering, so an upload larger than memory is still an upload.
Functions
parse()
http.multipart.parse(body, boundary: string, options: ?dict) -> MultipartData
Parses a multipart/form-data body (RFC 7578).
The boundary comes from the Content-Type header’s boundary parameter
and must be given; a body cannot be parsed without it.
Parameters
body(bytes)boundary(string)options(?dict) —max_parts(default 1000) andmax_file_size(default: unlimited)
Returns MultipartData
Raises ProtocolError if the body is not a well-formed multipart
message
Raises TooLargeError if a limit is exceeded
Classes
UploadedFile
class http.UploadedFile
One file received in a multipart/form-data body.
The content is held in memory. HttpServer’s max_body_size is what
bounds how much that can be, so a service accepting large uploads should
raise that limit deliberately rather than by accident.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
name | string | The form field name the file arrived under. |
filename | ?string | The filename the client claimed, exactly as sent. |
content_type | string | The Content-Type the client declared for the file, or 'application/octet-stream' when it declared none. |
headers | Headers | Every header that came with this part. |
content | bytes | The file’s contents. |
Constructor
http.UploadedFile(name, filename, content_type, headers, content)
UploadedFile.size()
http.UploadedFile.size() -> number
The size of the uploaded content in bytes.
Returns number
UploadedFile.safe_name()
http.UploadedFile.safe_name() -> string
The client’s filename reduced to a single path-safe segment: any
directory component is dropped, and anything outside letters, digits,
., - and _ becomes an underscore. A name that reduces to nothing,
or to a leading run of dots, comes back as 'unnamed'.
Returns string
UploadedFile.save_to()
http.UploadedFile.save_to(path: string)
Writes the content to path.
Parameters
path(string)
Returns — number: the number of bytes written
UploadedFile.to_text()
http.UploadedFile.to_text() -> string
The content decoded as UTF-8 text.
Returns string
UploadedFile.to_string()
http.UploadedFile.to_string()
MultipartData
class http.MultipartData
The result of parsing a multipart/form-data body.
Fields
| Field | Type | Description |
|---|---|---|
fields | dict | Plain form values, as name -> [value, ...]. |
files | dict | Uploaded files, as name -> [UploadedFile, ...]. |
Constructor
http.MultipartData()
MultipartData.field()
http.MultipartData.field(name: string, fallback) -> any
The first value submitted for the field name, or fallback.
Parameters
name(string)fallback(?any)
Returns any
MultipartData.file()
http.MultipartData.file(name: string) -> ?UploadedFile
The first file submitted under name, or nil.
Parameters
name(string)
Returns ?UploadedFile
MultipartData.to_dict()
http.MultipartData.to_dict() -> dict
The plain fields as a flat name -> value dictionary, keeping the first
value of any repeated name. Convenient when a form is known not to
repeat names.
Returns dict
MultipartBuilder
class http.MultipartBuilder
Builds a multipart/form-data request body.
The boundary is generated from the platform’s secure random source rather than a counter or a timestamp, since a boundary a peer can predict is a boundary a peer can inject into a field value and thereby forge extra parts.
var form = http.MultipartBuilder()
form.add_field('title', 'Holiday')
form.add_file('photo', 'beach.jpg', photo_bytes, 'image/jpeg')
client.post(url, form.build(), { 'Content-Type': form.content_type() })
Constructor
http.MultipartBuilder(boundary)
MultipartBuilder.content_type()
http.MultipartBuilder.content_type() -> string
The Content-Type header value this body must be sent with, boundary
parameter included.
Returns string
MultipartBuilder.boundary()
http.MultipartBuilder.boundary() -> string
The boundary string in use.
Returns string
MultipartBuilder.add_field()
http.MultipartBuilder.add_field(name: string, value)
Adds a plain form field.
Parameters
name(string)value(string|number|bool)
Returns — MultipartBuilder: this same instance, for chaining
MultipartBuilder.add_file()
http.MultipartBuilder.add_file(name: string, filename: string, content, content_type: ?string)
Adds a file part.
A filename that is not plain ASCII is additionally sent as an RFC 5987
filename* parameter, which is how a non-ASCII name survives the trip;
the plain filename is kept alongside it for recipients that only
understand that one.
Parameters
name(string) — the form field namefilename(string)content(bytes|string)content_type(?string) — defaults to'application/octet-stream'
Returns — MultipartBuilder: this same instance, for chaining
MultipartBuilder.build()
http.MultipartBuilder.build() -> bytes
Serialises every part added so far into a complete body.
Returns bytes
MultipartBuilder.length()
http.MultipartBuilder.length() -> number
How many parts have been added.
Returns number
2026, Richard Ore and Zuri contributors
http.negotiate
import http
httpexposes this ashttp.negotiate, soimport httpis enough and the names are called ashttp.negotiate.*.import http.negotiatereaches the same definitions directly.
Content negotiation: choosing what to send when the client has said what it prefers.
Accept, Accept-Encoding and Accept-Language all use the same
quality-value grammar, which parse_accept() reads and best_match()
scores against what the server can actually produce.
Functions
parse_accept()
http.negotiate.parse_accept(header)
Parses an Accept, Accept-Encoding, Accept-Language or
Accept-Charset header into entries, most preferred first.
Ordering follows RFC 9110 §12.5.1: by quality weight descending, then by
specificity, then by the order they were written. An entry with q=0 is
kept rather than dropped, because q=0 is an explicit refusal
(“anything but this”) and a caller needs to see it to honour it.
Parameters
header(?string)
Returns — list: AcceptEntry, most preferred first
quality_of()
http.negotiate.quality_of(header, candidate: string) -> number
The quality weight header assigns to candidate, honouring wildcards.
Returns -1 when the header names nothing that covers candidate at
all, which is different from a weight of 0 (an explicit refusal) and
lets a caller apply the different defaults the two cases call for.
Parameters
header(?string)candidate(string)
Returns number
best_match()
http.negotiate.best_match(header, available: list) -> ?string
Picks the entry of available the client would most like, or nil when
it would accept none of them.
available is in server preference order, which decides ties - the
usual and correct behaviour, since the server is the one that knows
which representation is cheapest or best.
negotiate.best_match(request.header('accept'), ['application/json', 'text/html'])
Parameters
header(?string)available(list)
Returns ?string
preferred_encoding()
http.negotiate.preferred_encoding(header, available: list) -> ?string
Picks a content coding for a response, given the request’s
Accept-Encoding.
identity - sending the body uncompressed - is acceptable unless the
client explicitly refused it, so this falls back to 'identity' rather
than to nil whenever it can, and only returns nil when the client
has refused identity too and offered nothing else this server has.
Parameters
header(?string)available(list) — the codings the server can produce, in preference order
Returns ?string
preferred_language()
http.negotiate.preferred_language(header, available: list) -> ?string
Picks a language from available using the request’s Accept-Language.
A tag matches a request for its prefix, so en-GB satisfies a request
for en (RFC 4647’s basic filtering). The reverse is not true: a
request for en-GB is not satisfied by plain en, though en will
still be picked if nothing better is on offer.
Parameters
header(?string)available(list)
Returns ?string
names_explicitly()
http.negotiate.names_explicitly(header, value: string) -> bool
Whether header names value outright rather than covering it with a
wildcard.
This is the difference between a client that asked for JSON and one that
sent Accept: followed by a bare wildcard and would take anything - a
distinction that matters when deciding what to give a client that
expressed no real preference.
Parameters
header(?string)value(string)
Returns bool
Classes
AcceptEntry
class http.negotiate.AcceptEntry
One entry of an Accept-style header: the value, its quality weight,
and any other parameters it carried.
Fields
| Field | Type | Description |
|---|---|---|
value | string | The value itself, lowercased - a media type, a coding, a language tag, a charset. |
quality | number | The quality weight from the q parameter, 0.0 to 1.0. |
params | dict | Any parameters other than q, e.g. the level of an old text/html;level=1. |
Constructor
http.negotiate.AcceptEntry(value, quality, params)
AcceptEntry.specificity()
http.negotiate.AcceptEntry.specificity() -> number
How specific this entry is, used to break ties between entries of equal quality: an exact type outranks a subtype wildcard, which in turn outranks the bare wildcard.
Returns number
AcceptEntry.to_string()
http.negotiate.AcceptEntry.to_string()
2026, Richard Ore and Zuri contributors
http.proxy
import http.proxy
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.proxy.*needsimport http.proxy.
ReverseProxy forwards a request to another server and streams the
response back; LoadBalancer spreads those forwards across several
backends and stops sending to one that is failing.
Hop-by-hop headers are stripped on the way through, and forwarding headers are added, so the backend can still tell who the original client was.
Classes
ReverseProxy
class http.ReverseProxy
A reverse proxy: takes a request this server received and passes it to an upstream, then passes the upstream’s response back.
var api = http.ReverseProxy('http://127.0.0.1:9000')
server.any('/api/' + '*path', @(request, response) {
api.handle(request, response)
})
Hop-by-hop headers are stripped in both directions, the forwarding headers an upstream needs to know who the real client is are added, and the response body is streamed rather than buffered - a proxy that holds a whole response in memory is a proxy that falls over on the first large download.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
upstream | string | The upstream base URL, e.g. 'http://127.0.0.1:9000'. |
strip_prefix | ?string | A path prefix stripped from the incoming request before it is passed upstream, so /api/users can reach an… |
host_header | ?string | What to send upstream as Host. |
forward_headers | bool | Whether to add X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host and RFC 7239’s Forwarded. |
trust_proxy | bool | Whether this proxy itself sits behind another one, and so should extend the forwarding chain it was given… |
timeout | number | How long to wait for the upstream, in milliseconds. |
on_request | ?function | Called with (request, upstream_request) before the request goes out, to adjust it. |
on_response | ?function | Called with (request, upstream_response) once the upstream’s head has arrived and before it is passed on. |
Constructor
http.ReverseProxy(upstream: string, options: ?dict)
Parameters
upstream(string) — the base URL to forward tooptions(?dict) — any of the fields above, plusclientto supply anHttpClientof your own
ReverseProxy.handle()
http.ReverseProxy.handle(request, response)
Forwards request upstream and writes the upstream’s response into
response.
A connection failure becomes 502 Bad Gateway and a timeout 504 Gateway Timeout,
which is what those statuses are for - they tell the client the failure
was between this server and the next, not in the request.
Parameters
request(HttpRequest)response(HttpResponse)
ReverseProxy.to_string()
http.ReverseProxy.to_string()
LoadBalancer
class http.LoadBalancer
Balances requests across several upstreams.
Selection is round-robin, and an upstream that fails is taken out of
rotation for recovery_time seconds rather than retried on every
request - which is what stops one dead backend from adding its full
connect timeout to a share of all traffic.
var pool = http.LoadBalancer([
'http://10.0.0.1:9000',
'http://10.0.0.2:9000',
])
server.any('/' + '*path', @(request, response) {
pool.handle(request, response)
})
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
recovery_time | number | How long a failed upstream stays out of rotation, in seconds. |
max_attempts | number | How many upstreams to try before giving up on a request. |
Constructor
http.LoadBalancer(upstreams: list, options: ?dict)
Parameters
upstreams(list) — base URLsoptions(?dict) — passed to everyReverseProxy, plusrecovery_timeandmax_attempts
LoadBalancer.handle()
http.LoadBalancer.handle(request, response)
Forwards request to the next healthy upstream.
Parameters
request(HttpRequest)response(HttpResponse)
LoadBalancer.healthy_count()
http.LoadBalancer.healthy_count() -> number
How many upstreams are currently in rotation.
Returns number
LoadBalancer.to_string()
http.LoadBalancer.to_string()
2026, Richard Ore and Zuri contributors
http.request
import http.request
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.request.*needsimport http.request.
HttpRequest: one inbound request, with its method, target, headers and
body, plus the query string and route parameters already parsed.
The query-string functions are separate because they are useful on their
own: build_query_string() is what constructs a URL to request, not
only what reads one.
Functions
parse_query_string()
http.parse_query_string(text) -> dict
Decodes an application/x-www-form-urlencoded string - a query string,
or a form body - into name -> [value, ...].
+ decodes to a space and %xx to the byte it names, both of which
apply to names as well as values. A parameter with no = maps to an
empty string rather than to nil, matching what every server-side form
API does with ?debug.
Parameters
text(?string)
Returns dict
build_query_string()
http.build_query_string(parameters: dict) -> string
Encodes parameters as an application/x-www-form-urlencoded string.
Values may be lists, in which case the name repeats.
Parameters
parameters(dict)
Returns string
Classes
HttpRequest
class http.HttpRequest
An HTTP request, in both directions.
On the server side an instance arrives fully parsed: method, path,
query, headers, cookies and body are all populated, with
form(), json_body() and files() decoding the body on first use
according to its Content-Type.
On the client side, HttpClient builds one of these for every request
it sends, so anything you can inspect on the server you can also set
before sending.
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode()
Fields
| Field | Type | Description |
|---|---|---|
method | string | The request method, uppercased: 'GET', 'POST', and so on. |
target | string | The request target exactly as it appeared on the request line, before any decoding - '/search?q=a%20b'. |
path | string | The path, percent-decoded and with its . and .. segments resolved. |
query_string | string | The query string, without the leading ?, or ''. |
query | dict | The decoded query parameters, as name -> [value, ...]. |
version | string | The protocol version: '1.0', '1.1', '2' or '3'. |
headers | Headers | The request header fields. |
cookies | dict | The cookies the client sent, as name -> value. |
params | dict | Path parameters captured by the route that matched, e.g. id for a route registered as /users/:id. |
body | bytes | The request body. |
remote_address | ?string | The address of the peer at the other end of the socket. |
secure | bool | Whether the request arrived over TLS. |
scheme | string | The scheme the request was made with: 'http' or 'https'. |
authority | ?string | The authority the request was addressed to, from HTTP/2’s :authority or HTTP/1.1’s Host header, port… |
context | dict | Free-form storage for whatever middleware wants to attach to a request - a resolved user, a request id, a… |
body_reader | ?BodyReader | The reader for the body, when the route asked to stream it rather than have it materialised. |
Constructor
http.HttpRequest(method, target, headers)
Parameters
method(?string)target(?string)headers(?Headers)
HttpRequest.set_target()
http.HttpRequest.set_target(target: string)
Sets the request target and re-derives path, query_string and
query from it.
The path is percent-decoded and then normalised, in that order. Doing it
the other way round is the classic traversal hole: %2e%2e%2f looks
like an ordinary segment until it is decoded.
Parameters
target(string)
HttpRequest.header()
http.HttpRequest.header(name: string, fallback) -> any
A request header value, or fallback when absent.
Parameters
name(string)fallback(?any)
Returns any
HttpRequest.query_param()
http.HttpRequest.query_param(name: string, fallback) -> any
The first value of the query parameter name, or fallback.
Parameters
name(string)fallback(?any)
Returns any
HttpRequest.param()
http.HttpRequest.param(name: string, fallback) -> any
A path parameter captured by the matched route, or fallback.
Parameters
name(string)fallback(?any)
Returns any
HttpRequest.cookie()
http.HttpRequest.cookie(name: string, fallback) -> any
A cookie value, or fallback.
Parameters
name(string)fallback(?any)
Returns any
HttpRequest.session()
http.HttpRequest.session() -> Session
This request’s session.
The session is put here by http.session.session(), so a server that
never registered that middleware has none, and asking for one says so
rather than handing back nil for a handler to misread as an empty
session.
request.session().set('account', account.id)
Returns Session
Raises SessionError if the session middleware is not registered
HttpRequest.content_type()
http.HttpRequest.content_type() -> ?string
The media type of the body, without parameters, or nil.
Returns ?string
HttpRequest.content_length()
http.HttpRequest.content_length() -> number
The declared body length, or -1 when the request carries no
Content-Length (which for a chunked body it will not).
Returns number
HttpRequest.text()
http.HttpRequest.text() -> string
The body decoded as UTF-8 text.
Returns string
HttpRequest.json_body()
http.HttpRequest.json_body() -> any
The body parsed as JSON.
Parses once and caches, so calling this in both a middleware and a handler costs one parse.
Returns any
Raises HttpError if the body is not valid JSON
HttpRequest.form()
http.HttpRequest.form() -> dict
The submitted form fields, as name -> value, from either an
application/x-www-form-urlencoded or a multipart/form-data body. A
body of neither type gives an empty dictionary rather than raising.
Repeated names keep their first value here; use form_all() when a
field can legitimately repeat.
Returns dict
HttpRequest.form_all()
http.HttpRequest.form_all() -> dict
The submitted form fields as name -> [value, ...], keeping every value
of a repeated name.
Returns dict
HttpRequest.form_field()
http.HttpRequest.form_field(name: string, fallback) -> any
A single form field’s first value, or fallback.
Parameters
name(string)fallback(?any)
Returns any
HttpRequest.files()
http.HttpRequest.files() -> dict
The files uploaded in a multipart/form-data body, as name -> [UploadedFile, ...].
Empty for any other body type.
Returns dict
HttpRequest.file()
http.HttpRequest.file(name: string) -> ?UploadedFile
The first file uploaded under name, or nil.
Parameters
name(string)
Returns ?UploadedFile
HttpRequest.input()
http.HttpRequest.input(source: ?string) -> dict
The request’s input as one dictionary, ready to hand to a validator.
With no argument, three sources are merged, each overriding the one before it:
- route parameters, from the pattern that matched 2. query string parameters 3. the body - a JSON object’s keys, or the submitted form fields
Pass source to take exactly one of them instead: 'params', 'query'
or 'body'.
A query or form field is a list on the wire, since a name may legally
repeat. A name carrying exactly one value is flattened to that value
here, so a rule expecting a string sees a string; a name carrying
several stays a list. A field that must always be a list, however many
values arrived, is better read through form_all() or query directly.
Uploaded files are not included - nothing a schema can say about a file
is expressible as a rule over its bytes. Reach them with file() and
check them by hand.
A JSON body that is not an object (an array, a bare string) contributes
nothing, since there are no names to merge; read it with json_body()
instead.
Parameters
source(?string) —'params','query','body', or'all'(the default)
Returns dict
Raises ValueError if source names something else
HttpRequest.validate()
http.HttpRequest.validate(schema, source: ?string)
Validates the request’s input against schema and returns the data that
was validated.
schema is anything with a check_or_raise() method - a
validate.Schema in practice:
import validate
var create_user = validate.schema({
name: validate.required().string().max_length(100),
email: validate.required().string().email(),
})
server.post('/users', @(request, response) {
var data = request.validate(create_user)
response.json(create_account(data), 201)
})
Failure raises the schema’s own error - validate.ValidationError
- carrying an
errorslist of{ field, message }. Catch it to turn it into whatever your API answers with:
catch {
var data = request.validate(create_user)
response.json(create_account(data), 201)
} as error {
response.json({ errors: create_user.group_errors(error.errors) }, 422)
}
Parameters
schema(Schema)source(?string) — asinput(); defaults to'all'
Returns — dict: the input that was validated
Raises ValidationError when the input does not satisfy the schema
Raises TypeError if schema is not an object that can validate
Note: This deliberately does not import the
validatemodule. Any object exposingcheck_or_raise(data)works, and a server that never validates anything never pays to load it.
HttpRequest.host()
http.HttpRequest.host() -> ?string
The host the request was addressed to, without any port.
Returns ?string
HttpRequest.port()
http.HttpRequest.port() -> number
The port the request was addressed to, from the authority, or the scheme’s default when the authority named none.
Returns number
HttpRequest.url()
http.HttpRequest.url() -> string
The full absolute URL this request names.
Returns string
HttpRequest.client_ip()
http.HttpRequest.client_ip(trust_proxy: ?bool, trusted: ?list) -> ?string
The originating client’s IP address.
Directly served, that is simply the peer’s address. Behind a reverse
proxy the peer is the proxy, and the real client is in X-Forwarded-For
or RFC 7239’s Forwarded - but those are request headers, which is to
say anyone can write anything in them.
So they are only consulted when trust_proxy says to, and the value
taken is the rightmost entry that is not itself one of trusted,
walking in from the proxy end. Taking the leftmost entry - the common
shortcut - hands an attacker whatever client IP they care to claim,
which matters the moment an IP is used for rate limiting, allowlisting
or audit.
Parameters
trust_proxy(?bool) — whether to consult forwarding headers at all; defaults tofalsetrusted(?list) — proxy addresses to skip over when walking the chain
Returns ?string
HttpRequest.accepts()
http.HttpRequest.accepts(type: string) -> bool
Whether the client would accept a response of media type type, per its
Accept header. A request with no Accept accepts anything.
Parameters
type(string)
Returns bool
HttpRequest.wants_json()
http.HttpRequest.wants_json() -> bool
Whether the client would rather have JSON than HTML - the usual way to decide whether an error should be rendered as a page or returned as an object.
Returns bool
HttpRequest.bearer_token()
http.HttpRequest.bearer_token() -> ?string
The token from an Authorization: Bearer ... header, or nil when
there is no such header or it carries a different scheme.
Returns ?string
HttpRequest.is_ajax()
http.HttpRequest.is_ajax() -> bool
Whether the request was made by client-side script, as reported by the
X-Requested-With header that the major JavaScript libraries set.
Returns bool
HttpRequest.is_upgrade()
http.HttpRequest.is_upgrade(protocol: ?string) -> bool
Whether the request asks to switch to another protocol - a WebSocket handshake, or an HTTP/2 upgrade.
Parameters
protocol(?string) — check for one specific protocol
Returns bool
HttpRequest.expects_continue()
http.HttpRequest.expects_continue() -> bool
Whether the client asked the server to acknowledge before it sends the
body (Expect: 100-continue).
Returns bool
HttpRequest.is_safe()
http.HttpRequest.is_safe() -> bool
Whether this method is defined to be safe: read-only, with no side effects the client is responsible for (RFC 9110 §9.2.1).
Returns bool
HttpRequest.is_idempotent()
http.HttpRequest.is_idempotent() -> bool
Whether this method is idempotent - repeating it has the same effect as making it once (RFC 9110 §9.2.2). This is what decides whether a client may retry a request after a connection failure.
Returns bool
HttpRequest.to_wire()
http.HttpRequest.to_wire() -> string
The full request head in wire format, as it would be sent on an HTTP/1.1 connection. Useful for logging and for debugging what actually went out.
Returns string
HttpRequest.to_string()
http.HttpRequest.to_string()
HttpRequest.to_json()
http.HttpRequest.to_json()
2026, Richard Ore and Zuri contributors
http.response
import http.response
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.response.*needsimport http.response.
HttpResponse: one response, whether it is being built by a handler or
read back from a server.
It carries the status, the headers and the body, and the helpers on it cover what a handler nearly always wants: JSON, a file, a redirect, or a stream written a chunk at a time.
Classes
HttpResponse
class http.HttpResponse
An HTTP response, in both directions: the thing a server builds and sends, and the thing a client receives and reads.
On the server side the useful surface is the writers - text(),
json(), html(), file(), redirect(), render() - each of which
sets a sensible Content-Type alongside the body, plus set_cookie()
and the caching helpers.
On the client side it is as_text(), as_dict(), is_ok() and
raise_for_status().
A body can be bytes held in memory, a file streamed from disk, or a
callback that produces bytes as it goes. The last two matter for a
server that has to serve something larger than it would like to hold in
memory; see file() and stream().
- printable — has a
@to_string(), soechoandprint()show something useful - serializable — has a
@to_json(), so it can be handed straight tojson.encode()
Fields
| Field | Type | Description |
|---|---|---|
status | number | The status code. |
reason | ?string | The reason phrase. |
version | string | The protocol version this response was received over, or will be sent with: '1.0', '1.1', '2' or '3'. |
headers | Headers | The response header fields. |
body | bytes | The body, as bytes. |
cookies | list | Cookies to be sent with this response. |
time_taken | number | How long the request that produced this response took, in milliseconds. |
redirects | number | How many redirects were followed to reach it. |
responder | ?string | The URL that finally answered, which differs from the requested one whenever a redirect was followed. |
certificate | ?PeerCertificate | The peer’s TLS certificate, on a response received over HTTPS. |
body_reader | ?BodyReader | The reader for the body, when the client was asked to stream the response rather than materialise it. |
request_method | ?string | The method of the request this answers. |
Constructor
http.HttpResponse(body, status, headers)
Parameters
body(?any) —bytesor astringto start the body withstatus(?number) — defaults to200headers(?Headers)
HttpResponse.write()
http.HttpResponse.write(data)
Appends to the response body.
Parameters
data(string|bytes)
Returns — HttpResponse: this same instance, for chaining
HttpResponse.text()
http.HttpResponse.text(content: string, status: ?number)
Replaces the body with content and sets Content-Type to text/plain; charset=utf-8.
Parameters
content(string)status(?number)
Returns — HttpResponse: this same instance, for chaining
HttpResponse.html()
http.HttpResponse.html(content: string, status: ?number)
Replaces the body with content and sets Content-Type to text/html; charset=utf-8.
Parameters
content(string)status(?number)
Returns — HttpResponse: this same instance, for chaining
HttpResponse.json()
http.HttpResponse.json(data, status: ?number)
Replaces the body with the JSON encoding of data and sets
Content-Type to application/json.
Parameters
data(any)status(?number)
Returns — HttpResponse: this same instance, for chaining
HttpResponse.xml()
http.HttpResponse.xml(content: string, status: ?number)
Replaces the body with content and sets Content-Type to
application/xml; charset=utf-8.
Parameters
content(string)status(?number)
Returns — HttpResponse: this same instance, for chaining
HttpResponse.file()
http.HttpResponse.file(path: string, offset: ?number, length: ?number)
Serves the file at path, streamed from disk rather than read into
memory, with Content-Type guessed from the extension.
offset and length serve part of the file, which is what a Range
request needs; the caller is responsible for setting the 206 status
and Content-Range header that go with it.
Parameters
path(string)offset(?number) — byte offset to start from; defaults to0length(?number) — how many bytes to send; defaults to the rest of the file
Returns — HttpResponse: this same instance, for chaining
Raises HttpError if the file does not exist or cannot be read
HttpResponse.download()
http.HttpResponse.download(path: string, name: ?string)
Serves the file at path as an attachment, so a browser saves it rather
than displaying it.
Parameters
path(string)name(?string) — the filename to offer; defaults to the file’s own base name
Returns — HttpResponse: this same instance, for chaining
HttpResponse.stream()
http.HttpResponse.stream(handler)
Produces the body from a callback instead of holding it in memory.
handler is called with a writer that has write(data) and flush().
Anything it writes goes out as it is written, which is what makes
server-sent events, a progress feed, or a large generated export
possible without buffering the whole thing first.
Unless a Content-Length is set beforehand, the body is framed with
chunked transfer encoding on HTTP/1.1 and as an ordinary DATA stream on
HTTP/2.
server.handle('GET', '/export', @(request, response) {
response.content_type('text/csv')
response.stream(@(writer) {
for row in rows {
writer.write(row.join(',') + '\n')
}
})
})
Parameters
handler(function(1))
Returns — HttpResponse: this same instance, for chaining
HttpResponse.render()
http.HttpResponse.render(path: string, variables: ?dict)
Renders a Wire template and appends the result to the body, setting
Content-Type to HTML if it is not already set.
Templates are resolved against Wire’s shared instance, whose root
defaults to a templates directory beside the working directory. Use
wire.shared().set_root() to point it elsewhere.
Parameters
path(string) — the template path, without the.htmlextensionvariables(?dict)
Returns — HttpResponse: this same instance, for chaining
HttpResponse.redirect()
http.HttpResponse.redirect(location: string, status: ?number)
Sends the client to location, setting both the Location header and a
3xx status.
The default is 302 Found, which browsers follow with a GET regardless
of the original method. Use 307 or 308 when the method and body must
be preserved, and 303 after a form submission that should not be
replayed on refresh.
Parameters
location(string)status(?number) — must be a 3xx; defaults to302
Returns — HttpResponse: this same instance, for chaining
Raises ValueError if status is not a redirect status
HttpResponse.content_type()
http.HttpResponse.content_type(mimetype: string)
Sets the Content-Type.
Parameters
mimetype(string)
Returns — HttpResponse: this same instance, for chaining
HttpResponse.header()
http.HttpResponse.header(name: string, value)
Sets a response header.
Parameters
name(string)value(string|number|bool)
Returns — HttpResponse: this same instance, for chaining
HttpResponse.set_cookie()
http.HttpResponse.set_cookie(name: string, value: string, attributes: ?dict)
Adds a cookie to the response.
http_only defaults to true and same_site to 'Lax', which are the
settings a session cookie should have; pass them explicitly to opt out.
secure defaults to true for any cookie whose name carries the
__Secure- or __Host- prefix.
Parameters
name(string)value(string)attributes(?dict) —domain,path,expires,max_age,secure,http_only,same_site,partitioned
Returns — HttpResponse: this same instance, for chaining
HttpResponse.clear_cookie()
http.HttpResponse.clear_cookie(name: string, attributes: ?dict)
Expires a cookie on the client by re-sending it empty with a past expiry.
The domain and path must match the ones the cookie was set with, or
the client will keep the original and simply store a second,
differently-scoped, empty one.
Parameters
name(string)attributes(?dict) —domainandpath
Returns — HttpResponse: this same instance, for chaining
HttpResponse.cache_for()
http.HttpResponse.cache_for(seconds: number, public_cache: ?bool)
Marks the response as cacheable for seconds, for both private and
shared caches.
Parameters
seconds(number)public_cache(?bool) — whether shared caches (a CDN, a proxy) may store it too; defaults totrue
Returns — HttpResponse: this same instance, for chaining
HttpResponse.no_cache()
http.HttpResponse.no_cache()
Tells every cache, including the browser’s, not to store this response. The three headers below are the combination that actually works across the caches still in service.
Returns — HttpResponse: this same instance, for chaining
HttpResponse.as_text()
http.HttpResponse.as_text() -> string
The body decoded as UTF-8 text, or '' for an empty body.
Returns string
HttpResponse.as_dict()
http.HttpResponse.as_dict() -> any
The body parsed as JSON.
Returns any
Raises HttpError if the body is empty or is not valid JSON
HttpResponse.as_bytes()
http.HttpResponse.as_bytes() -> bytes
The raw body bytes.
Returns bytes
HttpResponse.media_type()
http.HttpResponse.media_type() -> ?string
The media type from Content-Type, without its parameters, e.g.
'application/json' for 'application/json; charset=utf-8'.
Returns ?string
HttpResponse.charset()
http.HttpResponse.charset() -> ?string
The charset named by Content-Type, or nil.
Returns ?string
HttpResponse.is_ok()
http.HttpResponse.is_ok() -> bool
Whether the status is a 2xx.
Returns bool
HttpResponse.is_redirect()
http.HttpResponse.is_redirect() -> bool
Whether the status is a 3xx.
Returns bool
HttpResponse.is_error()
http.HttpResponse.is_error() -> bool
Whether the status is a 4xx or 5xx.
Returns bool
HttpResponse.raise_for_status()
http.HttpResponse.raise_for_status() -> HttpResponse
Raises StatusError when the status is a 4xx or 5xx, and returns the
response otherwise, so it can be used inline:
var data = http.get(url).raise_for_status().as_dict()
The response stays reachable on the raised error, so the body - where an API usually explains what went wrong - is not lost.
Returns HttpResponse
Raises StatusError
HttpResponse.reason_phrase()
http.HttpResponse.reason_phrase() -> string
The reason phrase, either the one received or the canonical one for the status code.
Returns string
HttpResponse.is_bodiless()
http.HttpResponse.is_bodiless() -> bool
Whether the response is defined to carry no body: a 1xx, 204 or
304 status, or any response to a HEAD request.
Returns bool
HttpResponse.source()
http.HttpResponse.source() -> ?list
The body source, when the body is not held in body: ['file', path, offset, length]
or ['stream', handler]. nil for an ordinary in-memory body.
Returns ?list
HttpResponse.set_carrier()
http.HttpResponse.set_carrier(carrier)
Records the connection a streamed response is still being read from. Set
by HttpClient; hand the response to HttpClient.finish() when you are
done reading it.
Parameters
carrier(?list)
HttpResponse.carrier()
http.HttpResponse.carrier() -> ?list
The connection a streamed response is still being read from, or nil.
Returns ?list
HttpResponse.is_committed()
http.HttpResponse.is_committed() -> bool
Whether the head has already been written to the wire, after which changing a header or the status has no effect.
Returns bool
HttpResponse.commit()
http.HttpResponse.commit()
Marks the response as committed. Called by the protocol writer; applications have no reason to.
HttpResponse.on_finish()
http.HttpResponse.on_finish(callback: function)
Registers callback to run once the response is final: after the
handler, every middleware and any error handling have had their say, so
status, the headers and the body are what the client receives.
Middleware that reports on a response, such as an access log, registers
here rather than reading the response when its next() returns. A
failure further in never returns there, and the status the client is
sent for it is only decided afterwards.
Callbacks run once each, in the order they were registered. One that raises is skipped over, so a broken reporter never costs the client its response.
Parameters
callback(function) — called with no arguments.
HttpResponse.finish()
http.HttpResponse.finish()
Runs every callback on_finish() registered, once. Called by the server
when the response is final; applications have no reason to.
HttpResponse.cookie_headers()
http.HttpResponse.cookie_headers() -> list
Every Set-Cookie field value this response will send.
Returns list
HttpResponse.to_string()
http.HttpResponse.to_string()
HttpResponse.to_json()
http.HttpResponse.to_json()
2026, Richard Ore and Zuri contributors
http.router
import http.router
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.router.*needsimport http.router.
Matching a request to a handler.
Router holds the route table, Route is one pattern, and RouteMatch
is the result of matching including whatever the path parameters
captured. Patterns support named parameters and wildcards, and the
router answers 405 rather than 404 when a path exists under a
different method.
Classes
Route
class http.Route
One registered route.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
method | string | The method this route answers, uppercased. |
pattern | string | The pattern it was registered with, e.g. '/users/:id'. |
handler | function | The function called when the route matches, taking the request and the response. |
name | ?string | An optional name, so a URL can be built from the route rather than written out again at every call site. |
Constructor
http.Route(method, pattern, handler, name)
Route.to_string()
http.Route.to_string()
RouteMatch
class http.router.RouteMatch
The result of asking a router about a request.
Fields
| Field | Type | Description |
|---|---|---|
route | ?Route | The matched route, or nil when nothing matched. |
params | dict | Path parameters captured from the pattern. |
allowed | list | When a path matched but the method did not, the methods that path does answer - which is exactly what a… |
Constructor
http.router.RouteMatch(route, params, allowed)
RouteMatch.is_match()
http.router.RouteMatch.is_match() -> bool
Whether a route was found.
Returns bool
RouteMatch.is_method_mismatch()
http.router.RouteMatch.is_method_mismatch() -> bool
Whether the path exists but not for the requested method, which calls
for a 405 rather than a 404.
Returns bool
Router
class http.Router
Matches request paths to handlers.
Patterns are made of literal segments, named parameters, and an optional trailing catch-all:
| Pattern | Matches | Captures |
|---|---|---|
/users | /users | |
/users/:id | /users/42 | id = '42' |
/users/:id/posts | /users/42/posts | id = '42' |
/static/ + *path | /static/css/site.css | path = 'css/site.css' |
A literal segment always beats a parameter, and a parameter always beats
a catch-all, so /users/new and /users/:id can coexist and the
specific one wins - regardless of which was registered first, which is a
property a list-of-patterns router cannot offer.
Matching happens over a trie, so a router with a thousand routes costs the same per request as one with ten.
Constructor
http.Router()
Router.add()
http.Router.add(method: string, pattern: string, handler, name: ?string) -> Route
Registers handler for method requests to pattern.
Parameters
method(string)pattern(string)handler(function(2)) — called with the request and responsename(?string) — a name to look the route up by later
Returns Route
Raises HttpError if the pattern is malformed, or if the same
method and pattern were already registered
Router.match()
http.Router.match(method: string, path: string) -> RouteMatch
Finds the route for method and path.
When the path matches but the method does not, the returned match
carries the methods that path does answer, so the caller can send a
405 with a correct Allow header instead of a misleading 404.
HEAD falls back to the GET route when no HEAD route was
registered, since RFC 9110 §9.3.2 defines HEAD as GET without the body,
and the response writer drops the body anyway.
Parameters
method(string)path(string)
Returns RouteMatch
Router.url_for()
http.Router.url_for(name: string, params: ?dict) -> string
Builds the path for a named route, substituting params into its
pattern.
router.add('GET', '/users/:id', show, 'user.show')
router.url_for('user.show', { id: 42 }) # '/users/42'
Parameters
name(string)params(?dict)
Returns string
Raises HttpError if no route has that name, or a parameter the
pattern needs was not supplied
Router.routes()
http.Router.routes() -> list
Every registered route.
Returns list
Router.length()
http.Router.length() -> number
How many routes are registered.
Returns number
2026, Richard Ore and Zuri contributors
http.server
import http.server
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.server.*needsimport http.server.
HttpServer: the server end of the module.
It accepts connections, speaks HTTP/1.1 or HTTP/2 as negotiated, runs the middleware chain around the matched handler, and serves static files when configured to. It can run single-threaded or hand connections to a pool of workers.
Classes
HttpServer
class http.HttpServer
An HTTP/1.1 server.
import http
var server = http.server(3000)
server.get('/', @(request, response) {
response.html('<h1>Hello</h1>')
})
server.get('/users/:id', @(request, response) {
response.json({ id: request.param('id') })
})
server.listen()
The pieces a server put in front of an application usually provides are
here rather than assumed: TLS with use_tls(), static files with
serve_files(), response compression, byte ranges, conditional
requests, keep-alive with bounded reuse, and limits on every part of a
request that a peer controls the size of.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
host | string | The address to bind to. |
port | number | The port to bind to. |
read_timeout | number | How long to wait for a request to arrive, in milliseconds. |
write_timeout | number | How long a write may block before the client is treated as gone. |
keep_alive_timeout | number | How long an idle keep-alive connection is held open, in milliseconds. |
max_keep_alive_requests | number | The most requests one connection may serve before it is closed. |
max_line_size | number | The longest request line, in bytes. |
max_header_size | number | The largest header section, in bytes. |
max_header_count | number | The most header fields one request may carry. |
header_timeout | number | How long the whole request head may take to arrive, in seconds, once its first byte has. |
max_body_size | number | The largest request body accepted, in bytes. |
server_name | string | The value sent in the Server header. |
compression | bool | Whether to compress responses whose media type benefits from it and whose client asked for it. |
compression_min_size | number | The smallest response worth compressing, in bytes. |
compression_quality | ?number | How hard to compress, on the scale of whichever coding content negotiation lands on - brotli 0-11,… |
trust_proxy | bool | Whether HttpRequest.client_ip() should consult forwarding headers. |
trusted_proxies | list | The proxy addresses to skip when walking a forwarding chain. |
socket | ?TcpStream | The listening socket, once listen() has bound it. |
http2 | bool | Whether to speak HTTP/2 when a client asks for it - through ALPN on a TLS connection, or through the… |
Constructor
http.HttpServer(port: ?number, host: ?string)
Parameters
port(?number) — defaults to8000host(?string) — defaults to'127.0.0.1'
HttpServer.handle()
http.HttpServer.handle(method: string, pattern: string, handler, name: ?string)
Registers handler for method requests matching pattern.
See Router for what a pattern may contain: literal segments, :name
parameters, and a trailing catch-all.
Parameters
method(string)pattern(string)handler(function(2)) — called with the request and responsename(?string) — a route name, forurl_for()
Returns — HttpServer: this same instance, for chaining
HttpServer.get()
http.HttpServer.get(pattern: string, handler, name: ?string)
Registers a GET handler. A HEAD request for the same path is answered by it too, with the body dropped.
Parameters
pattern(string)handler(function(2))name(?string)
Returns — HttpServer: this same instance, for chaining
HttpServer.post()
http.HttpServer.post(pattern: string, handler, name: ?string)
Registers a POST handler.
Parameters
pattern(string)handler(function(2))name(?string)
Returns — HttpServer: this same instance, for chaining
HttpServer.put()
http.HttpServer.put(pattern: string, handler, name: ?string)
Registers a PUT handler.
Parameters
pattern(string)handler(function(2))name(?string)
Returns — HttpServer: this same instance, for chaining
HttpServer.patch()
http.HttpServer.patch(pattern: string, handler, name: ?string)
Registers a PATCH handler.
Parameters
pattern(string)handler(function(2))name(?string)
Returns — HttpServer: this same instance, for chaining
HttpServer.delete()
http.HttpServer.delete(pattern: string, handler, name: ?string)
Registers a DELETE handler.
Parameters
pattern(string)handler(function(2))name(?string)
Returns — HttpServer: this same instance, for chaining
HttpServer.options()
http.HttpServer.options(pattern: string, handler, name: ?string)
Registers an OPTIONS handler, replacing the automatic one for this path.
Parameters
pattern(string)handler(function(2))name(?string)
Returns — HttpServer: this same instance, for chaining
HttpServer.head()
http.HttpServer.head(pattern: string, handler, name: ?string)
Registers a HEAD handler, replacing the automatic GET fallback for this path.
Parameters
pattern(string)handler(function(2))name(?string)
Returns — HttpServer: this same instance, for chaining
HttpServer.any()
http.HttpServer.any(pattern: string, handler)
Registers one handler for every method a route can carry.
Parameters
pattern(string)handler(function(2))
Returns — HttpServer: this same instance, for chaining
HttpServer.use()
http.HttpServer.use(middleware)
Adds a middleware, called for every request before the route handler.
A middleware takes (request, response, next) and must call next() to
let the rest of the chain run; not calling it is how a middleware
short-circuits, which is exactly what an authentication or rate-limiting
layer wants to do.
server.use(@(request, response, next) {
var started = time()
next()
echo '${request.method} ${request.path} ${response.status} ' +
'${(time() - started) * 1000}ms'
})
Middleware run in the order they were added, outermost first.
Parameters
middleware(function(3))
Returns — HttpServer: this same instance, for chaining
HttpServer.not_found()
http.HttpServer.not_found(handler)
Sets the handler called when no route matches. The default sends a plain
404, or a JSON one when the client asked for JSON.
Parameters
handler(function(2))
Returns — HttpServer: this same instance, for chaining
HttpServer.error_handler()
http.HttpServer.error_handler(handler)
Sets the handler called when a route handler raises.
It takes (request, response, error) and is responsible for producing
the response. Without one, the server sends a bare 500 and reports the
error through on_error() listeners - it never puts an exception
message in a response body, since that is how internal paths and query
fragments end up on a user’s screen.
Parameters
handler(function(3))
Returns — HttpServer: this same instance, for chaining
HttpServer.serve_files()
http.HttpServer.serve_files(prefix: string, directory: string, options: ?dict)
Serves the files under directory at URLs beginning with prefix.
server.serve_files('/static', './public', { cache_age: 86400 })
Parameters
prefix(string)directory(string)options(?dict) — passed through toStaticFiles-index_files,cache_age,etag,precompressed,allow_dotfiles,fallback
Returns — HttpServer: this same instance, for chaining
HttpServer.routes()
http.HttpServer.routes() -> Router
The router, for anything the convenience methods above do not cover - listing routes, or building a URL from a route name.
Returns Router
HttpServer.on_connect()
http.HttpServer.on_connect(listener)
Adds a listener called with the Connection each time a client
connects.
Parameters
listener(function(1))
Returns — HttpServer: this same instance, for chaining
HttpServer.on_disconnect()
http.HttpServer.on_disconnect(listener)
Adds a listener called with the Connection when a client disconnects.
Parameters
listener(function(1))
Returns — HttpServer: this same instance, for chaining
HttpServer.on_receive()
http.HttpServer.on_receive(listener)
Adds a listener called with (request, response) after a request has
been parsed and before it is routed.
Parameters
listener(function(2))
Returns — HttpServer: this same instance, for chaining
HttpServer.on_reply()
http.HttpServer.on_reply(listener)
Adds a listener called with (request, response) after a response has
been sent. This is where an access log belongs.
Parameters
listener(function(2))
Returns — HttpServer: this same instance, for chaining
HttpServer.on_error()
http.HttpServer.on_error(listener)
Adds a listener called with (error, connection) whenever serving a
connection fails.
Without at least one such listener, connection-level errors are swallowed: one client sending a malformed request must not take the server down with it.
Parameters
listener(function(2))
Returns — HttpServer: this same instance, for chaining
HttpServer.use_tls()
http.HttpServer.use_tls(cert_chain: string, private_key: string, options: ?dict)
Turns on TLS, using the given PEM-encoded certificate chain and private key.
cert_chain must be the server certificate followed by any
intermediates - leaving the intermediates out is the single most common
TLS misconfiguration, and it fails only for the clients that do not
happen to have them cached.
Parameters
cert_chain(string) — PEM, leaf firstprivate_key(string) — PEMoptions(?dict) —alpn(a list of protocol names),client_ca(PEM roots for mutual TLS),require_client_cert,min_versionandmax_version
Returns — HttpServer: this same instance, for chaining
Note: The certificate and key are parsed when the first handshake runs, not here, so a malformed or mismatched pair surfaces as a failed connection rather than as a failed call to this method.
HttpServer.load_certs()
http.HttpServer.load_certs(cert_path: string, key_path: ?string, options: ?dict)
Loads the certificate and key from files rather than strings.
Parameters
cert_path(string)key_path(?string) — defaults tocert_path, for a combined PEM fileoptions(?dict) — asuse_tls()
Returns — HttpServer: this same instance, for chaining
HttpServer.set_tls_config()
http.HttpServer.set_tls_config(config)
Uses a net.tls.TlsConfig built elsewhere, for anything use_tls()
does not expose.
Parameters
config(TlsConfig)
Returns — HttpServer: this same instance, for chaining
HttpServer.is_secure()
http.HttpServer.is_secure() -> bool
Whether this server is configured to serve over TLS.
Returns bool
HttpServer.listen()
http.HttpServer.listen()
Binds the socket and serves connections until close() is called.
Connections are served one at a time on the calling thread. For a server
that uses more than one core, see http.serve(), which runs this same
request pipeline across a pool of isolates.
Raises HttpError if the socket cannot be bound
HttpServer.bind()
http.HttpServer.bind()
Binds and starts listening without accepting anything, so a caller can drive the accept loop itself.
The listener is left in non-blocking mode, since accept() and
listen() here go through a net.Acceptor. A caller driving its own
loop over socket directly gets nil from accept() whenever nothing
is waiting.
Raises HttpError if the socket cannot be bound
HttpServer.accept()
http.HttpServer.accept() -> ?TcpStream
Accepts one connection, blocking until one arrives. Only valid after
bind().
Returns nil if the server is stopped while this is waiting, since
there is then nothing left to wait for.
Returns ?TcpStream
HttpServer.address()
http.HttpServer.address() -> ?SocketAddr
The address the server is listening on. After binding to port 0, this
is how to discover the port that was actually chosen.
Returns ?SocketAddr
HttpServer.is_listening()
http.HttpServer.is_listening() -> bool
Whether the server is currently listening.
Returns bool
HttpServer.close()
http.HttpServer.close()
Stops the server and closes the listening socket. The accept loop reads
the stopped flag on its next round and ends, within one net.Acceptor
interval of this call.
HttpServer.serve_connection()
http.HttpServer.serve_connection(client)
Serves every request on one already-accepted client socket, then closes it.
This is the whole per-connection pipeline - TLS handshake, keep-alive
loop, parsing, dispatch, response - and is public so that a caller
running its own accept loop (or handing sockets to worker isolates) can
reuse it exactly as listen() does.
Parameters
client(TcpStream)
HttpServer.accept_connection()
http.HttpServer.accept_connection(client)
Turns an accepted socket into a Connection, running the TLS handshake
when one is configured and notifying on_connect() listeners.
Split out of serve_connection() so that a caller driving its own event
loop can take a connection on without also committing to serving it to
completion on the spot: see serve_next().
Parameters
client(TcpStream)
Returns — ?Connection: nil when the handshake failed, in which
case the socket has already been closed and the failure reported to
on_error() listeners
HttpServer.serve_next()
http.HttpServer.serve_next(connection, served: ?number)
Reads and answers exactly one request, and reports whether the connection may be used again.
This is the unit an event loop works in. _serve_requests() calls it in
a loop for one connection at a time, which is what listen() does; a
polled worker calls it once per connection that has actually become
readable, which is what lets one thread hold many connections at once.
Parameters
connection(Connection)served(?number) — how many requests this connection has already answered, so the keep-alive budget and the shorter idle read timeout apply from the second request onwards
Returns — bool: whether to keep the connection open
HttpServer.serve_http2()
http.HttpServer.serve_http2(connection)
Runs an HTTP/2 session on connection until the peer goes away.
Unlike HTTP/1.1, this owns the connection for its whole life: an HTTP/2 connection is already multiplexed internally, so the streams on it are interleaved even though the connection itself is not interleaved with any other.
Parameters
connection(Connection)
HttpServer.is_http2_connection()
http.HttpServer.is_http2_connection(connection) -> bool
Whether a connection that has just been taken on is going to speak HTTP/2. Reading this costs a peek at the first bytes on a cleartext connection, so it is asked once and the answer kept.
Parameters
connection(Connection)
Returns bool
HttpServer.report_error()
http.HttpServer.report_error(error, connection)
Hands error to this server’s on_error() listeners.
Public so that a caller running its own connection loop reports failures through the same listeners the built-in loops use, instead of inventing a second place errors can appear.
Parameters
error(Error)connection(?Connection)
HttpServer.finish_connection()
http.HttpServer.finish_connection(connection)
Notifies on_disconnect() listeners and closes the connection.
Parameters
connection(Connection)
HttpServer.handle_request()
http.HttpServer.handle_request(request, response) -> HttpResponse
Runs one already-parsed request through the middleware chain, the router and the error handling, and returns the response.
This is the whole application-facing half of the server, with no reference to how the request arrived. That is what lets the same routes and middleware serve an HTTP/1.1 connection and an HTTP/2 stream without either protocol knowing about the other.
Parameters
request(HttpRequest)response(?HttpResponse) — an existing response to fill in
Returns HttpResponse
HttpServer.to_string()
http.HttpServer.to_string()
2026, Richard Ore and Zuri contributors
http.session
import http
httpexposes this ashttp.session, soimport httpis enough and the names are called ashttp.session.*.import http.sessionreaches the same definitions directly.
Server-side sessions: a small amount of state that belongs to one visitor, kept on the server and found again by a cookie the browser sends back.
Only the identifier travels. Whatever the session holds stays in a store the server controls, so a visitor can neither read it nor change it, and the cookie is worth nothing to anyone who cannot present the exact value that was issued.
Using it
import http
import http.session
var server = http.server(3000)
server.use(session.session())
server.post('/login', @(request, response) {
var account = authenticate(request.form())
# A new identifier, so a session identifier planted beforehand is
# not the one that ends up logged in.
request.session().regenerate()
request.session().set('account', account.id)
response.redirect('/')
})
server.get('/', @(request, response) {
var id = request.session().get('account', nil)
response.html(id == nil ? render_guest() : render_account(id))
})
server.listen()
session() is middleware, so it is registered once and every handler
below it can reach request.session().
Nothing happens until something uses it
A request that never touches its session costs nothing: no store read,
no store write, and no Set-Cookie. A store record is created the first
time something is written, which is what keeps a crawler working through
a public site from filling the store with empty sessions.
A request that only reads an existing session writes nothing back
either, beyond moving the idle expiry along at most once every
touch_interval seconds.
Where it is kept
FileStore is the default, in a private directory under the platform’s
temporary directory. It needs no setup and it is shared between the
workers http.serve() starts, which a store held in memory is not.
server.use(session.session({
store: session.FileStore('/var/lib/app/sessions'),
}))
A relational database works as well, from http.session.sql, which is a
separate import so that a program using the default store never loads
the sql module:
import http.session.sql { SqlStore }
import sql
var store = SqlStore(sql.pool('postgres://localhost/app'))
store.migrate()
server.use(session.session({ store }))
Anything else - Redis, a key-value service, a table of your own shape -
is a subclass of SessionStore.
Options
| Option | Default | Meaning |
|---|---|---|
store | a FileStore in the default directory | where sessions are kept |
name | 'zuri_session' | the cookie’s name |
path | '/' | the cookie’s path |
domain | nil | the cookie’s domain; nil scopes it to the exact host |
secure | nil | nil follows the request’s own scheme |
http_only | true | hides the cookie from scripts |
same_site | 'Lax' | 'Strict', 'Lax' or 'None' |
persistent | false | whether the cookie outlives the browser |
idle_timeout | 7200 | seconds of inactivity before a session ends |
lifetime | 86400 | seconds a session may live however active |
touch_interval | 60, or half idle_timeout when that is less | how often a read-only request moves the idle expiry |
max_size | 65536 | the largest payload a session may serialise to |
secret | nil | signs the cookie, so a forged one is refused without a store read |
gc_probability | 0.01 | passed to a default FileStore; ignored when store is given |
Two clocks
idle_timeout rolls forward while the visitor is active; lifetime
does not. A session ends at whichever comes first, so a tab left open
overnight is still asked to sign in again. Either may be nil to remove
that limit, but not both.
Signing the cookie
An identifier is 32 bytes from the platform’s cryptographic generator,
which is far beyond guessing. Signing adds nothing against that, and
everything against volume: with secret set, a cookie that was not
issued by this server is thrown out after one HMAC, rather than after a
read from disk or a query to the database. Set it on anything exposed to
the open internet.
import env
server.use(session.session({ secret: env.require('SESSION_SECRET') }))
Every worker must be given the same secret, or a cookie issued by one is refused by the next.
What a session may hold
Whatever JSON holds: strings, numbers, booleans, nil, lists and
dictionaries of those. A class instance is not JSON, and storing one
raises when the session is written rather than coming back as something
else.
Sessions are for identity and small state - who is signed in, which
steps of a form are done, what to say on the next page. A payload over
max_size raises SessionError; the answer is a row in a database with
the session holding its key.
Submodules
| Module | Reached as | Summary |
|---|---|---|
http.session.file | http.session.file.* | The session store that keeps one file per session. |
http.session.memory | http.session.memory.* | The session store that keeps everything in the isolate’s own heap. |
http.session.sql | import http.session.sql | The session store that keeps sessions in a relational database. |
http.session.store | http.session.store.* | What a session store is, and the error every one of them raises. |
Constants
FORMAT
http.session.FORMAT = 1
The payload format this module writes and reads.
A session written by a different version is ignored rather than guessed at, so an upgrade signs people out instead of handing a handler fields that are not what it expects.
ID_LENGTH
http.session.ID_LENGTH = 43
How many characters a session identifier has.
Thirty-two bytes from the platform’s cryptographic generator, in base64url without padding.
Functions
session()
http.session.session(options: ?dict) -> function(3)
Middleware that finds each request’s session and writes it back when the response goes out.
Register it once, above anything that reads a session:
server.use(session.session({ secret: env.require('SESSION_SECRET') }))
The session lands on request.context['session'], which
request.session() reads.
A handler that raises still has its session written, and the failure carries on to the error handler afterwards. That is what lets an error page show a flash the handler set before it failed.
Parameters
options(?dict) — see the table at the top of this module
Returns function(3)
Raises SessionError if an option is not one this module has, or
holds a value it cannot mean
Classes
Session
class http.session.Session
One visitor’s session.
A handler reaches its own through request.session() rather than
building one. The middleware makes it, hands it to the request, and
writes it back afterwards.
Nothing is read from the store until the first method that needs the contents is called, and nothing is written back unless something changed.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
http.session.Session(store, config: dict, presented: ?string, secure: ?bool)
Parameters
store(SessionStore)config(dict) — assession()resolved itpresented(?string) — the cookie value the client sent, if anysecure(?bool) — whether the request arrived over TLS
Session.id()
http.session.Session.id() -> ?string
The session’s identifier, or nil when it has none yet.
A session that has never been written to has no identifier, because nothing has been stored for one to name.
Returns ?string
Session.is_new()
http.session.Session.is_new() -> bool
Whether this request arrived without a session the store recognised - no cookie, a cookie that did not verify, or one naming a session that has expired or been destroyed.
Returns bool
Session.get()
http.session.Session.get(name: string, fallback) -> any
The value stored under name, or fallback when there is none.
Parameters
name(string)fallback(?any) — defaults tonil
Returns any
Session.set()
http.session.Session.set(name: string, value)
Stores value under name.
This is what brings a session into existence: the first set() on a new
session assigns an identifier, and the middleware writes the record and
sends the cookie when the response goes out.
Parameters
name(string)value(any) — anything JSON can carry
Returns — Session: this same instance, for chaining
Session.has()
http.session.Session.has(name: string) -> bool
Whether anything is stored under name.
Parameters
name(string)
Returns bool
Session.remove()
http.session.Session.remove(name: string)
Removes whatever is stored under name. Removing something that is not
there changes nothing.
Parameters
name(string)
Returns — Session: this same instance, for chaining
Session.all()
http.session.Session.all() -> dict
Everything the session holds, as a dictionary.
The dictionary is a copy: writing to it does not change the session.
Returns dict
Session.clear()
http.session.Session.clear()
Empties the session without ending it. The identifier, the cookie and the record all survive; only the contents go.
Use destroy() to end the session itself, which is what signing out
wants.
Returns — Session: this same instance, for chaining
Session.regenerate()
http.session.Session.regenerate()
Gives the session a new identifier, keeping everything it holds, and removes the record the old identifier named.
Call this the moment an account signs in. Without it, an attacker who can set a cookie in the victim’s browser beforehand - through a stray subdomain, an open redirect, a shared machine - knows the identifier the victim will be signed in under, and can simply use it. That is session fixation, and a new identifier is the whole of the defence.
It is also worth doing whenever what the session means changes: an elevation to administrator, a step-up authentication.
The absolute lifetime restarts here, since a new identifier begins a
new session; the contents carry across.
Returns — Session: this same instance, for chaining
Session.destroy()
http.session.Session.destroy()
Ends the session: removes the record, empties the contents, and has the response expire the cookie.
This is what signing out does. A handler may keep using the session object afterwards, and doing so starts a fresh session with a new identifier.
Returns — Session: this same instance, for chaining
Session.flash()
http.session.Session.flash(name: string, value)
Stores value under name for exactly the next request.
This is the message a redirect needs to carry: the handler that did the work knows what happened, and the page the visitor is sent to is the one that has to say so.
server.post('/posts', @(request, response) {
create_post(request.form())
request.session().flash('notice', 'Your post is up.')
response.redirect('/posts')
})
server.get('/posts', @(request, response) {
response.html(render(request.session().take_flash('notice')))
})
A flash is spent by the next request that touches the session at all, whether or not that request asks for this one. A page that looked at the session and did not read the message does not leave it for the page after. A request that never touched its session - a static file, an image - leaves it waiting, which is what keeps the message for the page it was meant for.
Parameters
name(string)value(any)
Returns — Session: this same instance, for chaining
Session.take_flash()
http.session.Session.take_flash(name: string, fallback) -> any
Reads the flash stored under name by the previous request and removes
it, so a second call in the same request does not get it again.
Parameters
name(string)fallback(?any) — defaults tonil
Returns any
Session.flashes()
http.session.Session.flashes() -> dict
Every flash the previous request left, as a dictionary, without taking any of them.
The dictionary is a copy.
Returns dict
Session.started_at()
http.session.Session.started_at() -> ?number
When the session was created, as epoch seconds, or nil when it has not
been created yet.
regenerate() restarts this.
Returns ?number
Session.touched_at()
http.session.Session.touched_at() -> ?number
When the session was last written or touched, as epoch seconds, or nil
when it does not exist yet.
A read-only request moves this at most once every touch_interval
seconds, so it is a coarse measure of activity rather than the time of
the last request.
Returns ?number
Session.expires_at()
http.session.Session.expires_at() -> ?number
When the session stops being valid, as epoch seconds, or nil when it
does not exist yet.
Returns ?number
Session.save()
http.session.Session.save()
Writes the session to the store now, if anything changed.
The middleware does this as the response goes out, so a handler rarely calls it. It is here for one that is about to do something long - stream a large body, wait on a slow service - and would rather not hold the change until afterwards.
Returns — bool: whether anything was written
Raises SessionError if the session serialises to more than
max_size bytes
Session.commit()
http.session.Session.commit(response)
Writes the session and puts whatever the client needs on the response: the cookie for a session that is new or has a new identifier, an expired cookie for one that was destroyed.
The middleware calls this once, after the rest of the chain has run. Calling it again does no harm and writes nothing more.
Parameters
response(HttpResponse)
Note: A response that has already started going out cannot carry a
Set-Cookie. A session whose identifier has just changed is then not written at all, since nothing could present it; an existing session is still written, and only its cookie is skipped. Start a session before streaming begins.
Session.to_string()
http.session.Session.to_string()
2026, Richard Ore and Zuri contributors
http.session.file
import http.session.file
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.session.file.*needsimport http.session.file.
The session store that keeps one file per session.
It is the default because it needs nothing set up: no database, no
daemon, no schema. It is also shared, which matters the moment a server
runs on more than one core - http.serve() puts every worker in its own
isolate, and a store held in memory would give each of them a different
set of sessions.
Constants
SUFFIX
http.session.file.SUFFIX = '.session'
DIRECTORY_MODE
http.session.file.DIRECTORY_MODE = 448
FILE_MODE
http.session.file.FILE_MODE = 384
Functions
default_directory()
http.session.default_directory() -> string
The directory sessions are kept in when a FileStore is not told where
to put them: a private subdirectory of the platform’s temporary
directory, named after the working directory of the program that asked.
The temporary directory itself is deliberately not used. It is
world-writable, and on a shared machine world-readable, so a session
file dropped straight into it can be read by every other account on the
host - which is the oldest way there is to be logged in as somebody
else. The subdirectory is created 0700 and checked before it is used.
Naming it after the working directory keeps two programs on one machine from sharing a session space by accident, and keeps the same program’s sessions across a restart.
Returns string
Note: The platform’s temporary directory is cleared on a schedule the platform decides, and on most of them at every reboot. Sessions kept here therefore survive a restart of the server but not necessarily a restart of the machine. Anything that has to outlive the host names its own directory.
Classes
FileStore
class http.session.FileStore < SessionStore
Keeps each session in its own file, named after the session’s storage key.
import http.session
server.use(session.session({
store: session.FileStore('/var/lib/app/sessions'),
}))
A file holds its expiry on the first line and the payload on the rest, so a sweep can decide whether to keep a session without understanding what is in it.
Writing
A write goes to a uniquely named file in the same directory and is then renamed over the real one, which the filesystem does atomically. A reader therefore sees either the previous session or the new one, never a half-written mixture, and a server killed mid-write leaves the previous session intact.
Two requests writing the same session at the same moment - the usual cause being parallel requests from one browser tab - both succeed, and the one that renames last is the one that survives. Nothing here serialises them.
Permissions
A session file is a bearer credential in the same way the cookie is. The
directory is created 0700 and each file 0600, and a directory that
anyone outside the owner can reach is refused outright rather than used.
Pass strict_permissions: false to accept one anyway, which is
occasionally what a deployment with its own access control wants and is
never what a default should do.
On Windows the mode check does not run, because the mode a file reports there does not describe who can open it.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
http.session.FileStore(directory: ?string, options: ?dict)
Parameters
directory(?string) — where to keep the files; defaults todefault_directory(). Created, with every parent it needs, if it is not already thereoptions(?dict) —gc_probability(default0.01) andstrict_permissions(defaulttrue)
Raises SessionError if the directory cannot be created, is not a
directory, or is reachable by users other than its owner
FileStore.directory()
http.session.FileStore.directory() -> string
The directory the sessions are in.
Returns string
FileStore.read()
http.session.FileStore.read(key: string)
FileStore.write()
http.session.FileStore.write(key: string, payload: string, expires_at: number)
FileStore.destroy()
http.session.FileStore.destroy(key: string)
FileStore.gc()
http.session.FileStore.gc(now: number)
FileStore.to_string()
http.session.FileStore.to_string()
2026, Richard Ore and Zuri contributors
http.session.memory
import http.session.memory
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.session.memory.*needsimport http.session.memory.
The session store that keeps everything in the isolate’s own heap.
Classes
MemoryStore
class http.session.MemoryStore < SessionStore
Keeps sessions in a dictionary, for as long as the isolate that made the store lives.
It is the right store for a test, where a session has to survive a handful of requests and nothing more, and for a single-process development server. It is the wrong store for anything serving real traffic:
- Nothing survives a restart. Every session in flight ends when the process does, deploy included.
- Nothing is shared.
http.serve()runs each worker in its own isolate with its own heap, so a browser whose next request lands on a different worker arrives with a session that worker has never heard of.
FileStore has neither problem and needs no more setup than this does,
which is why it, and not this, is the default.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
http.session.MemoryStore(options: ?dict)
Parameters
options(?dict) —gc_probability(default0.01)
MemoryStore.length()
http.session.MemoryStore.length() -> number
How many sessions are held, expired ones that have not yet been swept included.
Returns number
MemoryStore.clear()
http.session.MemoryStore.clear()
Drops every session at once.
MemoryStore.read()
http.session.MemoryStore.read(key: string)
MemoryStore.write()
http.session.MemoryStore.write(key: string, payload: string, expires_at: number)
MemoryStore.touch()
http.session.MemoryStore.touch(key: string, expires_at: number)
MemoryStore.destroy()
http.session.MemoryStore.destroy(key: string)
MemoryStore.gc()
http.session.MemoryStore.gc(now: number)
MemoryStore.to_string()
http.session.MemoryStore.to_string()
2026, Richard Ore and Zuri contributors
http.session.sql
import http.session.sql
httpdoes not re-export this module, so it is reached only by importing it directly.
The session store that keeps sessions in a relational database.
It lives in its own submodule, rather than alongside the other stores,
so that a program using the default file store never loads the sql
module or any of its adapters.
import http
import http.session
import http.session.sql { SqlStore }
import sql
var store = SqlStore(sql.pool('postgres://localhost/app'))
store.migrate()
server.use(session.session({ store }))
Constants
DEFAULT_TABLE
http.session.sql.DEFAULT_TABLE = 'sessions'
Classes
SqlStore
class http.session.sql.SqlStore < SessionStore
Keeps sessions in one table of a relational database.
The table is three columns - the storage key, the payload, and when the
session stops being valid - and migrate() creates it on whichever
engine is in use.
What it takes
Either a sql.Connection or a sql.Pool. A server wants the pool: a
connection is used by one isolate at a time, and the pool is what hands
each request one and takes it back.
A connection belongs to the isolate that opened it and cannot be shared,
so under http.serve() each worker builds its own pool and its own
store inside setup, rather than being handed one from outside.
Concurrency
Every write is a single statement, so a reader never sees half a session. Two requests writing the same session at the same moment both succeed and the later one wins. Nothing here takes a lock.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
http.session.sql.SqlStore(database, options: ?dict)
Parameters
database(Connection|Pool) — where the table livesoptions(?dict) —table(default'sessions') andgc_probability(default0.01)
Raises SessionError if the table name is not a plain identifier
SqlStore.table()
http.session.sql.SqlStore.table() -> string
The table sessions are kept in.
Returns string
SqlStore.migrate()
http.session.sql.SqlStore.migrate()
Creates the table and its index if they are not already there.
Safe to call on every start: an existing table is left exactly as it is, and nothing in it is touched.
The columns are portable rather than ideal for any one engine. An application that wants a different shape - a partitioned table, a different collation, an engine-specific TTL - creates the table itself and skips this.
| Column | Type | |
|---|---|---|
id | varchar(64) | the storage key, primary key |
payload | text | what the session holds |
expires_at | bigint | epoch seconds |
Returns — SqlStore: this same instance, for chaining
SqlStore.read()
http.session.sql.SqlStore.read(key: string)
SqlStore.write()
http.session.sql.SqlStore.write(key: string, payload: string, expires_at: number)
SqlStore.touch()
http.session.sql.SqlStore.touch(key: string, expires_at: number)
SqlStore.destroy()
http.session.sql.SqlStore.destroy(key: string)
SqlStore.gc()
http.session.sql.SqlStore.gc(now: number)
SqlStore.to_string()
http.session.sql.SqlStore.to_string()
2026, Richard Ore and Zuri contributors
http.session.store
import http.session.store
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.session.store.*needsimport http.session.store.
What a session store is, and the error every one of them raises.
A store is the only part of a session that touches durable state. It never sees a session identifier, only the key derived from one, and it never sees the session’s contents, only the opaque string the session layer serialised them into. That split is what lets a store be swapped without the rest of the stack knowing, and what lets a leaked store - a directory listing, a database dump, a backup tape - hand over nothing that can be replayed as a cookie.
Implementing one means five methods: read(), write(), destroy(),
gc(), and optionally touch(), which has a working default built on
the first two.
class RedisStore < session.SessionStore {
@new(client) {
self._client = client
}
read(key) {
return self._client.get('session:' + key)
}
write(key, payload, expires_at) {
var ttl = (expires_at - time()).ceil()
self._client.set_with_ttl('session:' + key, payload, ttl)
}
destroy(key) {
self._client.remove('session:' + key)
}
gc(now) {
# Redis expires keys itself.
return 0
}
}
Functions
storage_key()
http.session.storage_key(id: string) -> string
The key a store files a session under: the SHA-256 of the session identifier, as 64 lowercase hex characters.
Storing the digest rather than the identifier means the contents of a store cannot be turned back into cookies. Someone who reads the directory, the table, or a backup of either learns what is in the sessions but cannot resume one, because the value the browser presents is the preimage.
It also settles the shape of a key once and for all: 64 hex characters make a safe filename and a fixed-width primary key, whatever a client put in its cookie.
Parameters
id(string)
Returns string
Classes
SessionError
class http.session.SessionError < HttpError
Raised when a session cannot be read, written or configured: a storage directory that cannot be created or is open to other users on the machine, a payload larger than the configured ceiling, an unknown option, or a store method an implementation forgot.
It is an HttpError, so a server already catching those catches this as
well.
- printable — has a
@to_string(), soechoandprint()show something useful
SessionError.to_string()
http.session.SessionError.to_string()
SessionStore
class http.session.SessionStore
What every session store implements.
The methods here raise SessionError, so a store that forgets one fails
where the gap is rather than somewhere further down.
Every timestamp is epoch seconds, as time() reports them.
Fields
| Field | Type | Description |
|---|---|---|
gc_probability | number | The chance, between 0 and 1, that a write also sweeps expired sessions. |
SessionStore.read()
http.session.SessionStore.read(key: string) -> ?string
The payload stored under key, or nil when there is none or the one
there has expired.
A store that finds an expired entry removes it rather than leaving it for the sweep.
Parameters
key(string)
Returns ?string
SessionStore.write()
http.session.SessionStore.write(key: string, payload: string, expires_at: number)
Stores payload under key, replacing whatever was there, and records
when it stops being valid.
Parameters
key(string)payload(string)expires_at(number)
SessionStore.touch()
http.session.SessionStore.touch(key: string, expires_at: number)
Moves the expiry of an existing entry without rewriting its contents, which is what a rolling idle timeout does on a request that only read the session.
The default reads the payload and writes it back. A store whose backend
can change the expiry on its own - a single-column UPDATE, a TTL
command - overrides this with that.
Parameters
key(string)expires_at(number)
Returns — bool: whether an entry was there to touch
SessionStore.destroy()
http.session.SessionStore.destroy(key: string)
Removes the entry stored under key. Removing one that is not there is
not an error.
Parameters
key(string)
SessionStore.gc()
http.session.SessionStore.gc(now: number)
Removes every entry that expired on or before now.
Parameters
now(number)
Returns — number: how many entries were removed
SessionStore.maybe_gc()
http.session.SessionStore.maybe_gc()
Runs gc() with probability gc_probability, and otherwise does
nothing. A store calls this from its own write().
A sweep that fails raises out of the write that triggered it: a store that cannot sweep is a store that will not be able to write for much longer either, and swallowing that hides a full disk or a dead connection until the next outage.
Returns — bool: whether a sweep ran
SessionStore.close()
http.session.SessionStore.close()
Releases whatever the store holds open. The default does nothing, which is right for a store that holds nothing.
2026, Richard Ore and Zuri contributors
http.sse
import http
httpexposes this ashttp.sse, soimport httpis enough and the names are called ashttp.sse.*.import http.ssereaches the same definitions directly.
Server-sent events (the WHATWG text/event-stream format): a one-way
stream of named, identified messages that a browser’s EventSource
reconnects to on its own.
Where a WebSocket gives you a duplex connection and a framing protocol to go with it, this gives you a long-lived response body and nothing else - which is all a live feed of updates actually needs, and it survives proxies that would refuse an upgrade.
import http
import http.sse
server.get('/events', @(request, response) {
sse.stream(response, @(events) {
iter var i = 0; i < 10; i++ {
events.send({ tick: i }, 'tick')
os.sleep(1)
}
})
})
Functions
stream()
http.sse.stream(response, producer)
Turns response into a server-sent event stream and runs producer
against it.
The headers that matter are set here: text/event-stream, no caching,
and X-Accel-Buffering: no for the benefit of any proxy in front that
would otherwise hold the stream in a buffer and defeat the whole
exercise.
Parameters
response(HttpResponse)producer(function(1)) — called with anEventStream
last_event_id()
http.sse.last_event_id(request) -> ?string
The Last-Event-ID a reconnecting client sent, or nil.
A browser sends this automatically when EventSource reconnects, and it
is how a stream resumes where it left off instead of replaying from the
beginning.
Parameters
request(HttpRequest)
Returns ?string
parse()
http.sse.parse(text: string) -> list
Parses a text/event-stream body into a list of events, each a
dictionary with event, data, id and retry.
Useful for a client consuming a stream, and for testing one.
Parameters
text(string)
Returns list
Classes
EventStream
class http.sse.EventStream
The writer a server-sent event stream hands to its producer.
Every method flushes, because an event that sits in a buffer is an event that has not been sent.
Constructor
http.sse.EventStream(writer)
EventStream.send()
http.sse.EventStream.send(data, event: ?string, id: ?string)
Sends one event.
data may be a string, or any value, in which case it is JSON-encoded -
which is what a browser’s EventSource handler almost always parses it
back out of anyway. A multi-line string is split across several data:
lines, as the format requires.
Parameters
data(any)event(?string) — the event name, which a browser dispatches under; omitted means the defaultmessageeventid(?string) — the event id, which the browser sends back asLast-Event-IDwhen it reconnects
Returns — EventStream: this same instance, for chaining
EventStream.comment()
http.sse.EventStream.comment(text: ?string)
Sends a comment line, which a client ignores.
This is the standard way to keep an idle stream alive: a proxy that closes connections after a quiet minute cannot tell a comment from real traffic.
Parameters
text(?string)
Returns — EventStream: this same instance, for chaining
EventStream.set_retry()
http.sse.EventStream.set_retry(milliseconds: number)
Tells the client how long to wait before reconnecting, in milliseconds.
Parameters
milliseconds(number)
Returns — EventStream: this same instance, for chaining
EventStream.close()
http.sse.EventStream.close()
Ends the stream. The connection closes, and a browser will reconnect unless the response said otherwise.
EventStream.is_closed()
http.sse.EventStream.is_closed() -> bool
Whether the stream has been closed.
Returns bool
2026, Richard Ore and Zuri contributors
http.status
import http
httpexposes this ashttp.status, soimport httpis enough and the names are called ashttp.status.*.import http.statusreaches the same definitions directly.
The HTTP status codes registered with IANA, their canonical reason phrases, and a handful of predicates for asking what class a code belongs to.
The names are the constants; reason() turns a code into the phrase a
response line carries.
import http
echo http.status.NOT_FOUND # 404
echo http.status.reason(404) # Not Found
echo http.status.is_redirect(308) # true
Constants
CONTINUE
http.status.CONTINUE: int = 100
100 Continue. The client may proceed to send the request body it
announced with an Expect: 100-continue header.
SWITCHING_PROTOCOLS
http.status.SWITCHING_PROTOCOLS: int = 101
101 Switching Protocols. The server has accepted an Upgrade request;
everything after the response head speaks the new protocol. This is how
a WebSocket handshake completes.
PROCESSING
http.status.PROCESSING: int = 102
102 Processing (WebDAV, RFC 2518). An interim response telling a client the request is still being worked on.
EARLY_HINTS
http.status.EARLY_HINTS: int = 103
103 Early Hints (RFC 8297). Carries Link headers the client can act on
(preload, preconnect) before the final response arrives.
OK
http.status.OK: int = 200
200 OK.
CREATED
http.status.CREATED: int = 201
201 Created. A new resource exists; Location names it.
ACCEPTED
http.status.ACCEPTED: int = 202
202 Accepted. The request was valid and queued, but not acted on yet, and may still ultimately fail.
NON_AUTHORITATIVE_INFORMATION
http.status.NON_AUTHORITATIVE_INFORMATION: int = 203
203 Non-Authoritative Information. A transforming proxy modified the origin server’s response.
NO_CONTENT
http.status.NO_CONTENT: int = 204
204 No Content. Success, and deliberately no body. A response with this
status must never carry Content-Length or a body.
RESET_CONTENT
http.status.RESET_CONTENT: int = 205
205 Reset Content. Success; the client should reset the document view that sent the request.
PARTIAL_CONTENT
http.status.PARTIAL_CONTENT: int = 206
206 Partial Content. The body is the byte range(s) the request’s Range
header asked for.
MULTI_STATUS
http.status.MULTI_STATUS: int = 207
207 Multi-Status (WebDAV, RFC 4918). The body is an XML document carrying a separate status per sub-request.
ALREADY_REPORTED
http.status.ALREADY_REPORTED: int = 208
208 Already Reported (WebDAV, RFC 5842).
IM_USED
http.status.IM_USED: int = 226
226 IM Used (RFC 3229). The response is the result of applying one or more instance manipulations to the current resource.
MULTIPLE_CHOICES
http.status.MULTIPLE_CHOICES: int = 300
300 Multiple Choices.
MOVED_PERMANENTLY
http.status.MOVED_PERMANENTLY: int = 301
301 Moved Permanently. Clients and caches may rewrite the request method
to GET when following this, which is why 308 exists.
FOUND
http.status.FOUND: int = 302
302 Found. Historically (and still, in practice) followed by rewriting the method to GET.
SEE_OTHER
http.status.SEE_OTHER: int = 303
303 See Other. Follow with GET, always. This is the correct answer to a POST that should not be re-submitted on refresh.
NOT_MODIFIED
http.status.NOT_MODIFIED: int = 304
304 Not Modified. The cached representation the client already holds is still fresh. Carries no body.
USE_PROXY
http.status.USE_PROXY: int = 305
305 Use Proxy. Deprecated; clients are required to ignore it.
TEMPORARY_REDIRECT
http.status.TEMPORARY_REDIRECT: int = 307
307 Temporary Redirect. Like 302, but the method and body must be
preserved when following.
PERMANENT_REDIRECT
http.status.PERMANENT_REDIRECT: int = 308
308 Permanent Redirect. Like 301, but the method and body must be
preserved when following.
BAD_REQUEST
http.status.BAD_REQUEST: int = 400
400 Bad Request. The message could not be understood - malformed syntax, invalid framing, a header the server refuses to guess at.
UNAUTHORIZED
http.status.UNAUTHORIZED: int = 401
401 Unauthorized. Authentication is required or was rejected. A response
with this status must carry WWW-Authenticate.
PAYMENT_REQUIRED
http.status.PAYMENT_REQUIRED: int = 402
402 Payment Required.
FORBIDDEN
http.status.FORBIDDEN: int = 403
403 Forbidden. Understood and refused; re-authenticating won’t help. Use
401 when it would.
NOT_FOUND
http.status.NOT_FOUND: int = 404
404 Not Found.
METHOD_NOT_ALLOWED
http.status.METHOD_NOT_ALLOWED: int = 405
405 Method Not Allowed. The path exists but not for this method. A
response with this status must carry Allow.
NOT_ACCEPTABLE
http.status.NOT_ACCEPTABLE: int = 406
406 Not Acceptable. Nothing the server can produce matches the request’s
Accept headers.
PROXY_AUTHENTICATION_REQUIRED
http.status.PROXY_AUTHENTICATION_REQUIRED: int = 407
407 Proxy Authentication Required.
REQUEST_TIMEOUT
http.status.REQUEST_TIMEOUT: int = 408
408 Request Timeout. The client took too long to send a complete request.
CONFLICT
http.status.CONFLICT: int = 409
409 Conflict. The request conflicts with the current state of the resource.
GONE
http.status.GONE: int = 410
410 Gone. Like 404, but deliberately permanent.
LENGTH_REQUIRED
http.status.LENGTH_REQUIRED: int = 411
411 Length Required.
PRECONDITION_FAILED
http.status.PRECONDITION_FAILED: int = 412
412 Precondition Failed. A conditional header (If-Match,
If-Unmodified-Since, …) did not hold.
CONTENT_TOO_LARGE
http.status.CONTENT_TOO_LARGE: int = 413
413 Content Too Large. The body exceeds what the server will accept.
URI_TOO_LONG
http.status.URI_TOO_LONG: int = 414
414 URI Too Long.
UNSUPPORTED_MEDIA_TYPE
http.status.UNSUPPORTED_MEDIA_TYPE: int = 415
415 Unsupported Media Type. The body’s Content-Type isn’t one this
resource handles.
RANGE_NOT_SATISFIABLE
http.status.RANGE_NOT_SATISFIABLE: int = 416
416 Range Not Satisfiable. None of the ranges asked for overlap the resource.
EXPECTATION_FAILED
http.status.EXPECTATION_FAILED: int = 417
417 Expectation Failed. The request’s Expect header names something
the server cannot do.
IM_A_TEAPOT
http.status.IM_A_TEAPOT: int = 418
418 I’m a teapot (RFC 2324). Reserved, and not to be taken seriously, but registered often enough to be worth naming.
MISDIRECTED_REQUEST
http.status.MISDIRECTED_REQUEST: int = 421
421 Misdirected Request. The connection this request arrived on cannot serve the authority it names. Relevant to HTTP/2 connection coalescing.
UNPROCESSABLE_CONTENT
http.status.UNPROCESSABLE_CONTENT: int = 422
422 Unprocessable Content. Syntactically fine, semantically impossible - the usual answer to a validation failure.
LOCKED
http.status.LOCKED: int = 423
423 Locked (WebDAV, RFC 4918).
FAILED_DEPENDENCY
http.status.FAILED_DEPENDENCY: int = 424
424 Failed Dependency (WebDAV, RFC 4918).
TOO_EARLY
http.status.TOO_EARLY: int = 425
425 Too Early (RFC 8470). The server won’t risk processing a request that arrived in TLS early data.
UPGRADE_REQUIRED
http.status.UPGRADE_REQUIRED: int = 426
426 Upgrade Required. Carries an Upgrade header naming what the client
must switch to.
PRECONDITION_REQUIRED
http.status.PRECONDITION_REQUIRED: int = 428
428 Precondition Required (RFC 6585). The server insists the request be conditional, to avoid a lost update.
TOO_MANY_REQUESTS
http.status.TOO_MANY_REQUESTS: int = 429
429 Too Many Requests (RFC 6585). Rate limited; Retry-After says for
how long.
REQUEST_HEADER_FIELDS_TOO_LARGE
http.status.REQUEST_HEADER_FIELDS_TOO_LARGE: int = 431
431 Request Header Fields Too Large (RFC 6585).
UNAVAILABLE_FOR_LEGAL_REASONS
http.status.UNAVAILABLE_FOR_LEGAL_REASONS: int = 451
451 Unavailable For Legal Reasons (RFC 7725).
INTERNAL_SERVER_ERROR
http.status.INTERNAL_SERVER_ERROR: int = 500
500 Internal Server Error.
NOT_IMPLEMENTED
http.status.NOT_IMPLEMENTED: int = 501
501 Not Implemented. The server does not recognise the method at all -
not to be confused with 405, which is per-resource.
BAD_GATEWAY
http.status.BAD_GATEWAY: int = 502
502 Bad Gateway. An upstream this server proxies to returned something invalid.
SERVICE_UNAVAILABLE
http.status.SERVICE_UNAVAILABLE: int = 503
503 Service Unavailable. Temporary, by definition; pair it with
Retry-After.
GATEWAY_TIMEOUT
http.status.GATEWAY_TIMEOUT: int = 504
504 Gateway Timeout. An upstream did not answer in time.
HTTP_VERSION_NOT_SUPPORTED
http.status.HTTP_VERSION_NOT_SUPPORTED: int = 505
505 HTTP Version Not Supported.
VARIANT_ALSO_NEGOTIATES
http.status.VARIANT_ALSO_NEGOTIATES: int = 506
506 Variant Also Negotiates (RFC 2295).
INSUFFICIENT_STORAGE
http.status.INSUFFICIENT_STORAGE: int = 507
507 Insufficient Storage (WebDAV, RFC 4918).
LOOP_DETECTED
http.status.LOOP_DETECTED: int = 508
508 Loop Detected (WebDAV, RFC 5842).
NOT_EXTENDED
http.status.NOT_EXTENDED: int = 510
510 Not Extended (RFC 2774).
NETWORK_AUTHENTICATION_REQUIRED
http.status.NETWORK_AUTHENTICATION_REQUIRED: int = 511
511 Network Authentication Required (RFC 6585). The captive-portal status.
Functions
reason()
http.status.reason(code: int) -> string
The canonical reason phrase for code, e.g. 'Not Found' for 404.
An unregistered code is still perfectly legal on the wire - a client is
required to treat it as the generic x00 of its class - so rather than
failing, this falls back to the class name ('Informational',
'Success', 'Redirection', 'Client Error', 'Server Error'), or
'Unknown' for a code outside 100-599.
Parameters
code(int)
Returns string
is_registered()
http.status.is_registered(code: int) -> bool
Whether code is a registered status code with a canonical reason
phrase of its own.
Parameters
code(int)
Returns bool
is_informational()
http.status.is_informational(code: int) -> bool
Whether code is a 1xx interim status. These are consumed by the
protocol layer and never surface as the final response.
Parameters
code(int)
Returns bool
is_success()
http.status.is_success(code: int) -> bool
Whether code is a 2xx success status.
Parameters
code(int)
Returns bool
is_redirect()
http.status.is_redirect(code: int) -> bool
Whether code is a 3xx redirection status.
Parameters
code(int)
Returns bool
is_client_error()
http.status.is_client_error(code: int) -> bool
Whether code is a 4xx client error.
Parameters
code(int)
Returns bool
is_server_error()
http.status.is_server_error(code: int) -> bool
Whether code is a 5xx server error.
Parameters
code(int)
Returns bool
is_error()
http.status.is_error(code: int) -> bool
Whether code is any kind of error, client or server.
Parameters
code(int)
Returns bool
is_bodiless()
http.status.is_bodiless(code: int) -> bool
Whether a response carrying code is defined to have no body at all,
regardless of what headers say. RFC 9110 puts 1xx, 204 and 304 in
this category, and framing a body onto any of them is a protocol
violation rather than a stylistic choice.
Parameters
code(int)
Returns bool
preserves_method()
http.status.preserves_method(code: int) -> bool
Whether a redirect with code must keep the original method and body
when followed. True for 307 and 308; 301, 302 and 303 are all
conventionally rewritten to GET.
Parameters
code(int)
Returns bool
2026, Richard Ore and Zuri contributors
http.stream
import http.stream
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.stream.*needsimport http.stream.
The transport underneath everything else: a byte stream with buffering, timeouts and optional TLS.
Connection is what both the client and the server read and write
through, so the protocol layers above never have to know whether they
are talking over TCP or over TLS.
Functions
is_timeout_error()
http.stream.is_timeout_error(message) -> bool
Whether a transport error message describes a timeout (or a would-block, which a socket with a receive timeout reports as the same condition) rather than a genuine failure.
Parameters
message(string)
Returns bool
connect()
http.stream.connect(host: string, port: int, options: ?dict) -> Connection
Opens a connection to host on port, optionally wrapping it in TLS.
Timeouts are applied to the raw socket before any TLS handshake runs, which is both the only place they can be applied and what keeps a stalled handshake from hanging forever.
Parameters
host(string)port(int)options(?dict) —secure(bool),tls_config(anet.tls.TlsConfig),server_name(the SNI/verification name, defaulting tohost),connect_timeout,read_timeoutandwrite_timeout(all milliseconds)
Returns Connection
Raises ConnectionError, TimeoutError
tunnel()
http.stream.tunnel(proxy: dict, host: string, port: int, options: ?dict) -> Connection
Opens a connection to host:port through an HTTP proxy’s CONNECT
tunnel, running a TLS handshake with host inside it when secure is
set.
The proxy sees only where the tunnel goes. Everything sent through it
after the handshake is between this end and host, encrypted, and the
certificate checked is host’s own.
Parameters
proxy(dict) —hostandportof an HTTP proxy, andauthorization, theProxy-Authorizationvalue to send, ornil.host(string)port(int)options(?dict) — asconnect()takes them.
Returns Connection
Raises ConnectionError when the proxy cannot be reached or refuses
the tunnel, or the handshake through it fails
accept()
http.stream.accept(client, options: ?dict) -> Connection
Turns a freshly accepted TcpStream into a Connection, running a
server-side TLS handshake first when tls_config is given.
Parameters
client(TcpStream) — the streamTcpStream.accept()returnedoptions(?dict) —tls_config(anet.tls.TlsConfig; when present the connection is wrapped in TLS),read_timeoutandwrite_timeout(milliseconds)
Returns Connection
Raises ConnectionError if the TLS handshake fails
Classes
Connection
class http.Connection
A buffered, message-oriented view of a connected socket.
HTTP/1.1 is parsed a line at a time and then a body at a time, and
HTTP/2 a frame at a time; both want to read a few bytes without paying
for a syscall each time, and to write a head made of many small pieces
as one write. Connection sits between the protocol code and either a
plain net.TcpStream or a net.tls.TlsStream and provides exactly
that, plus the peer information the transports stop reporting once TLS
has consumed the underlying socket.
Instances are created by connect() and accept() below, or by
from_transport() when the socket already exists.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
peer_address | ?SocketAddr | The remote peer’s address, captured before any TLS wrap consumed the underlying socket. |
local_address | ?SocketAddr | This side’s address. |
secure | bool | Whether the connection is TLS-protected. |
alpn | ?string | The ALPN protocol negotiated during the TLS handshake ('h2', 'http/1.1'), or nil on a plaintext… |
Constructor
http.Connection(transport, peer, local)
Wraps an already-connected transport.
Parameters
transport(TcpStream|TlsStream)peer(?SocketAddr) — the peer address, which aTlsStreamcan no longer report on its ownlocal(?SocketAddr)
Connection.transport()
http.Connection.transport() -> TcpStream|TlsStream
The underlying TcpStream or TlsStream. Needed by the protocol
upgrade paths (WebSocket, HTTP/2) that take a connection over wholesale.
Returns TcpStream|TlsStream
Connection.peer_certificate()
http.Connection.peer_certificate() -> ?PeerCertificate
The peer’s certificate on a TLS connection, or nil on a plaintext one
or when the peer presented none.
Returns ?PeerCertificate
Connection.buffered()
http.Connection.buffered() -> number
How many bytes are already buffered and available without touching the socket.
Returns number
Connection.bytes_read()
http.Connection.bytes_read() -> number
How many bytes have been read off the transport over this connection’s whole life.
Returns number
Connection.is_eof()
http.Connection.is_eof() -> bool
Whether the peer has closed its side and the buffer is drained.
Returns bool
Connection.is_closed()
http.Connection.is_closed() -> bool
Whether close() has been called on this connection.
Returns bool
Connection.read_line()
http.Connection.read_line(limit)
Reads one CRLF-terminated line and returns it as a string with the terminator removed.
A bare LF is accepted as a line terminator as well. RFC 9112 §2.2 permits a recipient to do so, and enough real clients send one that refusing would break them; a bare CR, on the other hand, is left in the data where it belongs.
Parameters
limit(?number) — the longest line to tolerate, in bytes; defaults to 8192
Returns — ?string: the line, or nil at a clean EOF before any
bytes of a line arrived
Raises TooLargeError if the line exceeds limit
Raises ProtocolError if EOF arrives mid-line
Raises TimeoutError, ConnectionError
Connection.read_exactly()
http.Connection.read_exactly(length: number) -> bytes
Reads exactly length bytes.
Parameters
length(number)
Returns bytes
Raises ProtocolError if EOF arrives first
Raises TimeoutError, ConnectionError
Connection.read_some()
http.Connection.read_some(length: number)
Reads whatever is available, up to length bytes, blocking only until
the first byte arrives.
Parameters
length(number)
Returns — bytes: empty at EOF
Raises TimeoutError, ConnectionError
Connection.read_to_eof()
http.Connection.read_to_eof(limit) -> bytes
Reads until the peer closes its write half.
This is the framing of last resort - a response with neither
Content-Length nor chunked encoding is delimited by the close itself
(RFC 9112 §6.3). limit bounds how much will be accumulated so a peer
that never closes cannot exhaust memory.
Parameters
limit(?number) — the most to accept, in bytes;nilfor no limit
Returns bytes
Raises TooLargeError if limit is exceeded
Connection.discard()
http.Connection.discard(length: number)
Discards exactly length bytes without materialising them. Used to
drain the body of a request whose response has already been decided, so
the connection stays usable.
Parameters
length(number)
Raises ProtocolError if EOF arrives first
Connection.unread()
http.Connection.unread(data)
Pushes bytes back to the front of the read buffer so the next read sees them again. Used by protocol detection, which has to look at the first bytes of a connection before deciding which parser gets them.
Parameters
data(bytes)
Connection.write()
http.Connection.write(data)
Queues data for sending. Nothing reaches the socket until the buffer
fills or flush() is called.
Parameters
data(bytes|string)
Connection.flush()
http.Connection.flush()
Sends everything queued and blocks until the transport has taken all of it.
Raises TimeoutError, ConnectionError
Connection.set_read_timeout()
http.Connection.set_read_timeout(milliseconds)
Sets how long a read may block before raising TimeoutError. Only
effective on a plaintext connection; a TlsStream no longer owns a
socket whose timeout can be changed, so a TLS connection’s timeouts have
to be set on the TcpStream before it is wrapped (which connect() and
accept() below both do).
Parameters
milliseconds(number) —0or negative to block indefinitely
Connection.set_write_timeout()
http.Connection.set_write_timeout(milliseconds)
Sets how long a write may block before raising TimeoutError. Carries
the same TLS caveat as set_read_timeout().
Parameters
milliseconds(number)
Connection.set_nodelay()
http.Connection.set_nodelay(enabled)
Turns off Nagle’s algorithm, so a response head goes out immediately rather than waiting for more data to coalesce with. Every HTTP server wants this; a request is answered by a burst that is then followed by silence, which is the exact case Nagle handles badly.
Parameters
enabled(bool)
Connection.close()
http.Connection.close()
Flushes anything still queued and closes the transport. Safe to call more than once. A failure while flushing is swallowed - the connection is going away regardless, and the caller has usually already handled whatever went wrong.
Connection.to_string()
http.Connection.to_string()
2026, Richard Ore and Zuri contributors
http.util
import http
httpexposes this ashttp.util, soimport httpis enough and the names are called ashttp.util.*.import http.utilreaches the same definitions directly.
The small, exact pieces of the HTTP specifications that several parts of the module need: date formatting, percent-encoding, quoted strings, header parameter lists, path normalisation and authorization headers.
secure_equals() and random_token() are here because they are used
where timing matters: comparing a token in constant time, and generating
one that cannot be guessed.
Functions
find_bytes()
http.util.find_bytes(haystack, needle, from) -> number
Finds the first occurrence of the byte sequence needle in haystack,
at or after from, or -1 when it does not occur.
The built-in bytes.index_of() searches for a single byte, which is not
enough for the delimiters HTTP is full of - CRLF, the blank line ending
a head, a multipart boundary. This finds candidate positions with that
fast single-byte search and only then compares the rest.
Parameters
haystack(bytes)needle(bytes)from(?number)
Returns number
to_hex()
http.util.to_hex(n: number) -> string
n as lowercase hexadecimal, with no 0x prefix and no padding.
Parameters
n(number)
Returns string
format_date()
http.util.format_date(timestamp) -> string
Formats a Unix timestamp as an IMF-fixdate, the one date format RFC 9110
§5.6.7 requires every HTTP sender to produce: Sun, 06 Nov 1994 08:49:37 GMT.
Always UTC, always fixed-width, never localised. The two obsolete
formats are accepted by parse_date() but never emitted.
Parameters
timestamp(?number) — seconds since the epoch; defaults to now
Returns string
parse_date()
http.util.parse_date(text: string)
Parses any of the three date formats RFC 9110 §5.6.7 requires a recipient to accept, and returns the Unix timestamp.
The two obsolete ones (RFC 850’s Sunday, 06-Nov-94 08:49:37 GMT and
asctime’s Sun Nov 6 08:49:37 1994) still appear in the wild from old
servers and caches, so a client that only understands IMF-fixdate will
silently mishandle their Expires and Last-Modified.
Two-digit years follow the usual rule: a year that would place the date more than fifty years in the future is read as the previous century.
Parameters
text(string)
Returns — ?number: the timestamp, or nil if the date is
unparseable
parse_parameters()
http.util.parse_parameters(value: string)
Splits a header value that carries parameters - a media type, a
Content-Disposition, a challenge - into its leading value and a
dictionary of lowercased parameter names to values.
Quoted strings are unquoted and their backslash escapes resolved. RFC
5987 extended parameters (filename*=UTF-8''na%C3%AFve.txt) are decoded
and take precedence over the plain form of the same name, which is what
lets a non-ASCII upload filename survive.
parse_parameters('text/html; charset=utf-8')
# ['text/html', {charset: 'utf-8'}]
Parameters
value(string)
Returns — list: [value, parameters]
unquote()
http.util.unquote(value: string) -> string
Removes the surrounding double quotes from a header value and resolves its backslash escapes. A value that isn’t quoted comes back unchanged.
Parameters
value(string)
Returns string
quote()
http.util.quote(value: string) -> string
Wraps value in double quotes, escaping any quote or backslash it
contains, so it can be used as an RFC 9110 quoted-string.
Parameters
value(string)
Returns string
percent_decode()
http.util.percent_decode(text: string, plus_as_space: ?bool) -> string
Percent-decodes a URI component, turning + into a space only when
plus_as_space is set - which is right for a query string or a form
body, and wrong for a path, where + is a literal plus.
Invalid escapes are left as-is rather than raising: a malformed % in a
path is a 404 waiting to happen, not a reason to drop the connection.
Parameters
text(string)plus_as_space(?bool)
Returns string
percent_encode()
http.util.percent_encode(text: string) -> string
Percent-encodes every character of text that is not an RFC 3986
unreserved character, over the UTF-8 encoding of the input.
Parameters
text(string)
Returns string
normalize_path()
http.util.normalize_path(path: string)
Resolves the . and .. segments of a path and collapses repeated
slashes, returning a path that cannot climb above /.
This is the check that stands between a static file handler and GET /static/../../etc/passwd,
and it has to happen after percent-decoding, since %2e%2e%2f is the
same request written to survive a naive check.
Parameters
path(string)
Returns — string: always starting with /
secure_equals()
http.util.secure_equals(a, b) -> bool
Compares two strings without leaking, through how long the comparison takes, where they first differ.
Use this for anything an attacker can submit repeatedly and tune - a session token, an API key, an HMAC - where a byte-at-a-time comparison hands over the secret one guess at a time.
Parameters
a(string)b(string)
Returns bool
random_token()
http.util.random_token(length) -> string
A random lowercase-hex token of length characters, drawn from the
platform’s cryptographically secure generator.
Used for multipart boundaries, WebSocket keys, and anywhere else this module needs a value an attacker must not be able to predict.
Parameters
length(?number) — defaults to 32
Returns string
parse_basic()
http.util.parse_basic(header)
Parses an HTTP Basic Authorization field value into a username and
password.
The scheme name is matched case-insensitively, as RFC 9110 §11.1
requires. A header that is not a well-formed Basic credential - wrong
scheme, undecodable base64, no colon in the decoded pair - comes back as
nil rather than raising, since a malformed credential is a 401, not
a crash.
Parameters
header(?string)
Returns — ?list: [username, password], or nil
parse_bearer()
http.util.parse_bearer(header)
Parses a Bearer Authorization field value into its token.
Parameters
header(?string)
Returns — ?string: the token, or nil when the header is absent or
is not a bearer credential
is_valid_method()
http.util.is_valid_method(value) -> bool
Whether value is a valid HTTP method: a non-empty token, per RFC 9110
§9. Methods are case-sensitive, so no normalisation happens here.
Parameters
value(string)
Returns bool
2026, Richard Ore and Zuri contributors
http.websocket
import http.websocket
httplifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledhttp.websocket.*needsimport http.websocket.
WebSocket (RFC 6455), both ends of it.
accept() upgrades an inbound request, connect() opens an outbound
connection, and WebSocket is the resulting full-duplex channel.
Message is one complete message, reassembled from however many frames
carried it, and the CLOSE_* constants are the status codes a close may
carry.
Constants
OPCODE_CONTINUATION
http.websocket.OPCODE_CONTINUATION = 0
Continuation frame. @type int
OPCODE_TEXT
http.websocket.OPCODE_TEXT = 1
Text frame. @type int
OPCODE_BINARY
http.websocket.OPCODE_BINARY = 2
Binary frame. @type int
OPCODE_CLOSE
http.websocket.OPCODE_CLOSE = 8
Close frame. @type int
OPCODE_PING
http.websocket.OPCODE_PING = 9
Ping frame. @type int
OPCODE_PONG
http.websocket.OPCODE_PONG = 10
Pong frame. @type int
CLOSE_NORMAL
http.websocket.CLOSE_NORMAL = 1000
Normal closure. @type int
CLOSE_GOING_AWAY
http.websocket.CLOSE_GOING_AWAY = 1001
The endpoint is going away. @type int
CLOSE_PROTOCOL_ERROR
http.websocket.CLOSE_PROTOCOL_ERROR = 1002
A protocol error was detected. @type int
CLOSE_UNSUPPORTED
http.websocket.CLOSE_UNSUPPORTED = 1003
A message of a kind this endpoint cannot accept. @type int
CLOSE_INVALID_PAYLOAD
http.websocket.CLOSE_INVALID_PAYLOAD = 1007
A text message that was not valid UTF-8. @type int
CLOSE_POLICY_VIOLATION
http.websocket.CLOSE_POLICY_VIOLATION = 1008
A message that violates a policy. @type int
CLOSE_TOO_LARGE
http.websocket.CLOSE_TOO_LARGE = 1009
A message too large to process. @type int
CLOSE_INTERNAL_ERROR
http.websocket.CLOSE_INTERNAL_ERROR = 1011
An unexpected condition on the server. @type int
Functions
accept_key()
http.websocket.accept_key(key: string) -> string
The value a server must return in Sec-WebSocket-Accept for a given
client key.
Parameters
key(string) — the client’sSec-WebSocket-Key
Returns string
is_handshake()
http.websocket.is_handshake(request) -> bool
Whether request is a well-formed WebSocket handshake.
Parameters
request(HttpRequest)
Returns bool
accept()
http.websocket.accept(request, response, options: ?dict) -> WebSocket
Completes a WebSocket handshake and takes over the connection.
Call this from an ordinary route handler. It writes the 101 response
itself and hands back a WebSocket; the server will not write anything
more on that connection afterwards.
server.get('/ws', @(request, response) {
var socket = websocket.accept(request, response)
# ... talk to the socket from here
})
Parameters
request(HttpRequest)response(HttpResponse)options(?dict) —protocols(the subprotocols this server supports, most preferred first),max_message_size
Returns WebSocket
Raises HttpError if the request is not a WebSocket handshake, or
arrived on a connection that cannot be taken over
connect()
http.websocket.connect(target: string, options: ?dict) -> WebSocket
Opens a WebSocket connection to target.
Parameters
target(string) — aws://orwss://URLoptions(?dict) —headers(extra request headers),protocols(subprotocols to offer),tls_config,connect_timeout,max_message_size
Returns WebSocket
Raises HttpError if the server does not complete the handshake
Classes
Message
class http.websocket.Message
One complete WebSocket message, with any fragmentation already reassembled.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
opcode | int | The opcode of the message: OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING or OPCODE_PONG. |
data | bytes | The payload. |
Constructor
http.websocket.Message(opcode, data)
Message.is_text()
http.websocket.Message.is_text() -> bool
Whether this is a text message.
Returns bool
Message.is_binary()
http.websocket.Message.is_binary() -> bool
Whether this is a binary message.
Returns bool
Message.is_close()
http.websocket.Message.is_close() -> bool
Whether this is a close frame, meaning the peer has begun the closing handshake.
Returns bool
Message.text()
http.websocket.Message.text() -> string
The payload decoded as UTF-8 text.
Returns string
Message.close_code()
http.websocket.Message.close_code() -> ?number
The close code carried by a close frame, or nil when the frame carried
no code (which RFC 6455 §7.1.5 says to read as 1005, “no status
received”).
Returns ?number
Message.close_reason()
http.websocket.Message.close_reason() -> string
The human-readable reason carried by a close frame.
Returns string
Message.to_string()
http.websocket.Message.to_string()
WebSocket
class http.WebSocket
An open WebSocket connection (RFC 6455).
Created by accept() on the server side or connect() on the client
side, never directly.
server.get('/ws', @(request, response) {
var socket = websocket.accept(request, response)
while socket.is_open() {
var message = socket.receive()
if message == nil {
break
}
socket.send('you said: ' + message.text())
}
})
Ping frames are answered automatically, and a close frame from the peer is answered and then reported to the caller, so the closing handshake completes without the application having to know the rules.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
is_client | bool | Whether this endpoint is the client, and so must mask every frame it sends. |
protocol | ?string | The subprotocol negotiated during the handshake, or nil. |
max_message_size | number | The largest message this endpoint will assemble, in bytes. |
Constructor
http.WebSocket(connection, is_client, protocol)
WebSocket.is_open()
http.WebSocket.is_open() -> bool
Whether the connection is still open.
Returns bool
WebSocket.connection()
http.WebSocket.connection() -> Connection
The underlying transport, for anything this class does not expose - a read timeout, the peer’s address, its certificate.
Returns Connection
WebSocket.send()
http.WebSocket.send(text: string)
Sends a text message.
Parameters
text(string)
WebSocket.send_binary()
http.WebSocket.send_binary(data)
Sends a binary message.
Parameters
data(bytes)
WebSocket.ping()
http.WebSocket.ping(data)
Sends a ping. A conforming peer answers with a pong carrying the same payload, which is how an idle connection is kept alive through a NAT or a proxy that would otherwise time it out.
Parameters
data(?bytes) — at most 125 bytes
WebSocket.pong()
http.WebSocket.pong(data)
Sends a pong. Only needed to answer a ping this class did not answer for you, or as an unsolicited heartbeat.
Parameters
data(?bytes)
WebSocket.close()
http.WebSocket.close(code: ?number, reason: ?string)
Begins the closing handshake and closes the transport.
Parameters
code(?number) — a close code; defaults toCLOSE_NORMALreason(?string) — at most 123 bytes once encoded
WebSocket.receive()
http.WebSocket.receive() -> ?Message
Reads the next message, reassembling fragments and answering pings along the way.
Returns nil when the connection closes without a close frame. A close
frame is returned as a message, and answered first, so the caller can
see the code and reason the peer sent.
Returns ?Message
Raises ProtocolError if the peer violates the framing rules
WebSocket.to_string()
http.WebSocket.to_string()
2026, Richard Ore and Zuri contributors
http.worker
import http
httpexposes this ashttp.worker, soimport httpis enough and the names are called ashttp.worker.*.import http.workerreaches the same definitions directly.
The multi-process side of HttpServer: a pool of isolates, each
accepting and serving connections from the same listening socket.
Each worker polls the connections it holds rather than blocking on one,
so a slow client occupies a descriptor and not a thread. This is what
HttpServer uses when it is configured with more than one worker.
Functions
worker_main()
http.worker.worker_main(connections, setup, options)
The loop each worker isolate runs: build a server of its own from
setup, then serve whatever connections the acceptor hands it.
This lives in a module because it names imported ones of its own
(HttpServer, net). A module value cannot be handed to an isolate,
but a function belonging to a module is resolved there by name, with its
imports resolved again on that side.
Parameters
connections(Channel) — accepted sockets arrive heresetup(function(1)) — called once with this worker’s serveroptions(dict)
serve()
http.worker.serve(setup, options: ?dict)
Binds a listening socket and serves it across a pool of worker isolates.
The calling isolate does nothing but accept, which is cheap; every
accepted socket is moved across a channel to whichever worker takes it
next. Because isolates share no memory, each worker builds its own
routes by calling setup.
backlog is how many accepted-but-unclaimed connections may queue
before the acceptor blocks. Bounding it is deliberate: an unbounded
queue under load means accepting connections faster than they can be
served and answering all of them late, rather than letting the kernel’s
own listen backlog apply back-pressure.
on_ready is called once in this isolate with the bound address and a
function that stops the server, which is what a signal handler needs in
order to shut down cleanly:
http.serve(app.setup, {
port: 3000,
on_ready: @(address, stop) {
echo 'listening on ${address}'
os.on_signal('INT', @() {
stop()
return true
})
},
})
Parameters
setup(function(1))options(?dict) —port,host,workers,backlog,on_ready,max_connections(stop after serving that many),cert_chain,private_key, plus anyHttpServerfield to apply to every worker
Raises HttpError if the socket cannot be bound
Note: Each worker holds an isolate pool thread for as long as the server runs.
serve()sizes the pool for that when it is the first thing in the process to use an isolate; when it is not - because something already spawned one, fixing the pool’s size - it raises rather than deadlocking behind workers that never finish. Callisolate.configure()yourself, first thing, if the process needs isolates for anything besides serving.
2026, Richard Ore and Zuri contributors
rpc
import rpc
JSON-RPC 2.0: calling methods on another program, and answering its calls, over HTTP, WebSockets, sockets, pipes, or any stream of bytes.
JSON-RPC is a small, stateless protocol for calling methods in another
program. It defines the messages and nothing more: which methods exist
is up to the programs using it, and the messages travel over whatever
connects them. A message is a small JSON object: a request names a
method and carries its parameters and an id, the response carries the
same id and a result or an error, and a notification is a request
nobody answers. Either side may send any of them, so the protocol has no
fixed client and server.
The module is built in layers, each usable on its own:
Request,Notification,ResponseandRpcErrorare the messages, andencode(),decode()andread()turn them to and from JSON, checking every rule of the specification.- A
Serviceholds the handler for each method a program serves, and answers every message it is given with them. - Over HTTP,
http_handler()serves a service from a route of anhttpserver, andHttpClientcalls one. HeaderFraming,LineFramingandMessageFramingtell messages apart in a stream: by aContent-Lengthheader in front of each, one to a line, or one to each read of a transport that keeps them apart itself.- A transport carries the bytes of a connection:
StdioTransportover the program’s own standard streams,SocketTransportover a socket,ProcessTransportto a child process,WebSocketTransportover a WebSocket, and the two ends of apipe()between isolates. - An
Endpointis a service on a connection: it answers what arrives, and calls the other side with requests and notifications of its own. serve()gives every connection to a listening socket an endpoint of its own, across a pool of isolates.
import rpc
import isolate
var ends = rpc.pipe()
isolate.spawn(@(transport) {
import rpc
rpc.endpoint(transport)
.on_request('greet', @(params) => 'Hello, ${params.name}!')
.serve()
}, ends[1])
var client = rpc.endpoint(ends[0])
echo client.request('greet', { name: 'Zuri' }) # Hello, Zuri!
client.close()
The rpc API
Every public name in rpc, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
rpc.ChannelTransport | class | Talks over two isolate channels: what it reads arrives on inbox and what it writes goes to outbox. |
rpc.Context | class | What a handler is told about the message it is handling: the request’s id, its method, the endpoint… |
rpc.DEFAULT_BATCH_LIMIT | constant | The most messages a batch may hold before it is refused whole, 1000, until Service.set_batch_limit() says… |
rpc.DEFAULT_MAX_SIZE | constant | The largest message a framing accepts unless told otherwise: 64 MiB. |
rpc.Endpoint | class | One side of a JSON-RPC connection over a transport. |
rpc.HeaderFraming | class | Frames each message with a header block giving its length: |
rpc.HttpClient | class | Calls a JSON-RPC service over HTTP: each call is one POST to url, and its answer is the response. |
rpc.INTERNAL_ERROR | constant | The code for a request that failed while it was being handled, -32603. |
rpc.INVALID_PARAMS | constant | The code for a request whose parameters the method cannot take, -32602. |
rpc.INVALID_REQUEST | constant | The code for valid JSON that is not a valid JSON-RPC message, -32600. |
rpc.LineFraming | class | Frames each message as one line, ended by \n, the framing of newline-delimited JSON. |
rpc.METHOD_NOT_FOUND | constant | The code for a request naming a method the other side does not have, -32601. |
rpc.MessageFraming | class | Takes each piece it is fed as one whole message, for a transport that keeps messages apart itself, such as a… |
rpc.Notification | class | A call that expects no answer: the method to run and its params. |
rpc.PARSE_ERROR | constant | The code for a message that is not valid JSON, -32700. |
rpc.ProcessTransport | class | Talks to a child process over its standard input and output, from os.spawn() with stdin and stdout both… |
rpc.Request | class | A call that expects an answer: the method to run, its params, and the id the answer comes back with. |
rpc.Response | class | The answer to a Request: its id, and either the result the method returned or the error it failed… |
rpc.RpcClosedError | class | Raised by Endpoint.request() when the connection ends before the answer arrives, and by sending on an… |
rpc.RpcError | class | A JSON-RPC error: what a request failed with, on either side of a connection. |
rpc.RpcFramingError | class | Raised when the framing of a stream cannot be read: a header block that is not ASCII, a header without a… |
rpc.RpcHttpError | class | Raised by an HttpClient when the server answers with an HTTP status that carries no JSON-RPC answer:… |
rpc.RpcTimeoutError | class | Raised by Endpoint.request() when its timeout runs out before the answer arrives. |
rpc.SERVER_ERROR_MAX | constant | The highest code JSON-RPC sets aside for errors a server defines itself, -32000. |
rpc.SERVER_ERROR_MIN | constant | The lowest code JSON-RPC sets aside for errors a server defines itself, -32099. |
rpc.Service | class | The methods a program serves, and the answering of every message sent to them. |
rpc.SocketTransport | class | Talks over a connected stream from net: a TcpStream, a UnixStream or a TlsStream, or anything else… |
rpc.StdioTransport | class | Talks over the program’s own standard input and output: what it reads arrives on stdin, and what it writes… |
rpc.WebSocketTransport | class | Talks over a WebSocket from http.websocket, on either end of it: one websocket.accept() returned in a… |
rpc.decode | function | Decodes the JSON text of a message, or of a batch, and reads it as read() does. |
rpc.encode | function | The JSON text of a message, or of a list of messages sent as a batch. |
rpc.endpoint | function | A new Endpoint over transport, framing its messages with a HeaderFraming. |
rpc.http_handler | function | A route handler for an http server that answers JSON-RPC with service: register it for POST on whatever… |
rpc.pipe | function | Two ChannelTransports joined to each other: what one writes, the other reads. |
rpc.process | function | A transport to a child process, over its standard input and output. |
rpc.read | function | Reads a value decoded from JSON as a message: a Request, a Notification or a Response. |
rpc.serve | function | Serves JSON-RPC on a listening socket, across a pool of worker isolates, until it is stopped. |
rpc.socket | function | A transport over a connected socket from net. |
rpc.stdio | function | A transport over the program’s own standard input and output. |
rpc.websocket | function | A transport over a WebSocket from http.websocket. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
rpc.endpoint | rpc.* | Endpoint: one side of a JSON-RPC connection. |
rpc.error | rpc.* | The error codes JSON-RPC defines and the errors rpc raises, kept in a module of their own so every other… |
rpc.framing | rpc.* | How messages are told apart in a stream of bytes. |
rpc.http | rpc.* | JSON-RPC over HTTP, both ends of it. |
rpc.message | rpc.* | The messages JSON-RPC 2.0 exchanges, a Request, a Notification and a Response, and turning them to and… |
rpc.server | rpc.* | Serving JSON-RPC to many connections at once, over TCP, TLS or a Unix domain socket. |
rpc.service | rpc.* | Service: what a program answers. |
rpc.transport | rpc.* | Where an endpoint’s bytes come from and go to. |
Functions
endpoint()
rpc.endpoint(transport) -> Endpoint
A new Endpoint over transport, framing
its messages with a HeaderFraming.
Parameters
transport(any) — A transport; see {rpc.transport}.
Returns Endpoint
stdio()
rpc.stdio() -> StdioTransport
A transport over the program’s own standard input and output.
Returns StdioTransport
socket()
rpc.socket(socket) -> SocketTransport
A transport over a connected socket from net.
Parameters
socket(any) — A connectedTcpStream,UnixStreamorTlsStream.
Returns SocketTransport
process()
rpc.process(child) -> ProcessTransport
A transport to a child process, over its standard input and output.
Parameters
child(os.Process) — A child spawned withstdinandstdoutboth'pipe'.
Returns ProcessTransport
websocket()
rpc.websocket(socket) -> WebSocketTransport
A transport over a WebSocket from http.websocket.
Parameters
socket(http.websocket.WebSocket) — An open WebSocket, fromwebsocket.accept()orwebsocket.connect().
Returns WebSocketTransport
2026, Richard Ore and Zuri contributors
rpc.endpoint
import rpc
Everything here is re-exported by
rpc, soimport rpcis enough and the names are called asrpc.*. Importingrpc.endpointon its own works too and reaches the same definitions.
Endpoint: one side of a JSON-RPC connection. It is a
Service on a connection: it answers the
requests and notifications the other side sends, and sends its own.
JSON-RPC makes no difference between a client and a server, so the same
endpoint can do both on one connection.
Re-exported by rpc.
Classes
Endpoint
class rpc.Endpoint < Service
One side of a JSON-RPC connection over a transport.
An endpoint is a Service, so its handlers,
its fallback and its batch limit work exactly as a service’s do. The
connection adds the other direction: an endpoint calls the other side
with request(), request_batch(), send_request() and notify(),
and a handler on one can defer() its answer and send it later.
import rpc
import isolate
var ends = rpc.pipe()
# The server runs on an isolate of its own, answering until the
# client closes its end.
isolate.spawn(@(transport) {
import rpc
rpc.endpoint(transport)
.on_request('add', @(params) => params[0] + params[1])
.serve()
}, ends[1])
var client = rpc.endpoint(ends[0])
echo client.request('add', [2, 3]) # 5
client.close()
An endpoint reads its transport in one of three ways. serve() reads
and answers until the connection ends, on the isolate that calls it.
listen() reads on an isolate of its own and returns a Channel of
what arrives, for a program that waits on other things as well; it
passes each one to dispatch() when it is ready to handle it. A program
that reads the transport itself, such as one polling many connections,
hands what it reads to feed(). Every handler runs on the isolate that
owns the endpoint, and everything is sent from there.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.Endpoint(transport)
Returns a new Endpoint over transport. Its messages are
framed the way the transport keeps them apart: with the transport’s own
framing() when it has one, as a WebSocket does, and with a
HeaderFraming otherwise.
Parameters
transport(any) — A transport; seerpc.transport.
Endpoint.set_framing()
rpc.Endpoint.set_framing(framing) -> Endpoint
Sets how messages are told apart in the stream. Set it before
listen(), which hands the framing to the isolate it reads on.
Parameters
framing(HeaderFraming|LineFraming|MessageFraming) — Default the transport’s ownframing(), orHeaderFraming().
Returns Endpoint — itself.
Endpoint.notify()
rpc.Endpoint.notify(method: string, params)
Sends a notification: calls method on the other side and expects
nothing back.
Parameters
method(string)params(list|dict|nil) — Defaultnil, which sends none.
Raises RpcClosedError when the endpoint has been closed.
Endpoint.send_request()
rpc.Endpoint.send_request(method: string, params, callback: function)
Sends a request and returns at once; callback is called with the
answer when it arrives, on the isolate that owns the endpoint, while it
serves, waits on a request, or dispatches.
Parameters
method(string)params(list|dict|nil)callback(function(2)) — Called with the result andnil, or withniland the error: an {RpcError} the other side answered with, or an {RpcClosedError} when the connection ended first.
Returns — int: The request’s id.
Raises RpcClosedError when the endpoint has been closed.
Endpoint.request()
rpc.Endpoint.request(method: string, params, timeout: ?number)
Sends a request and waits for its answer, handling whatever else arrives
in the meantime as serve() would.
var sum = endpoint.request('add', [2, 3])
An error answer that names no request, which the other side sends for a
message it cannot read at all, goes to on_error() rather than to any
one request.
Parameters
method(string)params(list|dict|nil)timeout(?number) — The most seconds to wait. Defaultnil, which waits until the answer comes or the connection ends. A timeout needs the endpoint to be listening; one reading its transport itself waits as long as the transport’s reads do.
Returns — any: The result the other side answered with.
Raises RpcError when the other side answers with an error.
Raises RpcClosedError when the endpoint has been closed, or the
connection ends before the answer.
Raises RpcTimeoutError when timeout runs out first.
Endpoint.request_batch()
rpc.Endpoint.request_batch(calls: list, timeout: ?number)
Sends several requests as one batch and waits for every answer, handling
whatever else arrives in the meantime as request() does. The other
side may answer them in any order; they come back in the order of
calls.
var sums = endpoint.request_batch([['add', [1, 2]], ['add', [3, 4]]])
Parameters
calls(list) — Each call as a pair,[method, params].timeout(?number) — The most seconds to wait for every answer, as forrequest().
Returns — list: For each call, in order, the result the other side answered with, or the {RpcError} it answered with.
Raises ValueError when calls is empty, or a call is not a
[method, params] pair.
Raises RpcClosedError when the endpoint has been closed, or the
connection ends before every answer.
Raises RpcTimeoutError when timeout runs out first.
Endpoint.send()
rpc.Endpoint.send(message)
Sends a message as it is: a Request, a
Notification, a
Response, or a list of them as a batch.
Parameters
message(Request|Notification|Response|list)
Raises RpcClosedError when the endpoint has been closed.
Endpoint.serve()
rpc.Endpoint.serve()
Reads and answers messages until the connection ends, stop() is
called, or the endpoint is closed. On a listening endpoint, it takes
what arrives from the Channel listen() returned.
A message that is not valid UTF-8, or not valid JSON, is answered with
PARSE_ERROR and an id of nil.
Raises RpcFramingError when the stream cannot be read past a
message.
Endpoint.stop()
rpc.Endpoint.stop()
Makes serve() return once the message it is handling is done.
Endpoint.listen()
rpc.Endpoint.listen() -> isolate.Channel
Starts reading the transport on an isolate of its own, and returns the
Channel each message is put on as it arrives, already decoded from
JSON. Pass each item taken from it to dispatch(), on the isolate that
owns the endpoint, when the program is ready to handle it.
var inbox = endpoint.listen()
while !endpoint.is_closed() {
var ready = isolate.select([inbox, work], 0.5)
if ready[0] == inbox {
endpoint.dispatch(ready[1])
}
}
Each item is a dictionary whose kind says what arrived:
{ kind: 'message', text, value }: a message, as its JSON text and the value decoded from it.{ kind: 'unreadable', text, message }: a message that is not valid JSON, withtextnilwhen it is not valid UTF-8 either, and the parse error it is answered with.{ kind: 'closed' }: the end of the connection.{ kind: 'failed', message }: a failure reading the transport, which ends the connection.
Calling listen() again returns the same Channel.
Returns isolate.Channel
Raises ValueError when the transport cannot be read from another
isolate; see can_listen() in rpc.transport.
Endpoint.dispatch()
rpc.Endpoint.dispatch(item: dict)
Handles one item taken from the Channel listen() returned: answers a
message, or marks the connection ended. A failed connection is reported
to on_error() with an RpcClosedError.
Parameters
item(dict)
Endpoint.handle()
rpc.Endpoint.handle(value)
Handles one message already decoded from JSON, or a batch of them, exactly as if it had just arrived, and sends whatever answer it has.
Parameters
value(any) — A value asjson.decode()returns it.
Endpoint.feed()
rpc.Endpoint.feed(data: bytes)
Handles every message data completes, for a program that reads the
transport itself rather than through serve() or listen(), such as
one polling many connections. Empty data ends the connection, as an
empty read does.
Parameters
data(bytes) — The next bytes read from the transport.
Raises RpcFramingError when the stream cannot be read past a
message.
Endpoint.close()
rpc.Endpoint.close()
Closes the transport. Nothing more can be sent, and every request still
waiting is answered with an
RpcClosedError.
Endpoint.is_closed()
rpc.Endpoint.is_closed() -> bool
True once the endpoint is closed or the connection has ended.
Returns bool
Endpoint.to_string()
rpc.Endpoint.to_string() -> string
This endpoint as Endpoint(transport).
Returns string
2026, Richard Ore and Zuri contributors
rpc.error
import rpc
Everything here is re-exported by
rpc, soimport rpcis enough and the names are called asrpc.*. Importingrpc.erroron its own works too and reaches the same definitions.
The error codes JSON-RPC defines and the errors rpc raises, kept in a
module of their own so every other part of rpc can use them.
Re-exported by rpc.
Constants
PARSE_ERROR
rpc.PARSE_ERROR
The code for a message that is not valid JSON, -32700.
INVALID_REQUEST
rpc.INVALID_REQUEST
The code for valid JSON that is not a valid JSON-RPC message, -32600.
METHOD_NOT_FOUND
rpc.METHOD_NOT_FOUND
The code for a request naming a method the other side does not have,
-32601.
INVALID_PARAMS
rpc.INVALID_PARAMS
The code for a request whose parameters the method cannot take,
-32602.
INTERNAL_ERROR
rpc.INTERNAL_ERROR
The code for a request that failed while it was being handled, -32603.
SERVER_ERROR_MIN
rpc.SERVER_ERROR_MIN
The lowest code JSON-RPC sets aside for errors a server defines itself,
-32099. The range runs up to SERVER_ERROR_MAX.
SERVER_ERROR_MAX
rpc.SERVER_ERROR_MAX
The highest code JSON-RPC sets aside for errors a server defines itself,
-32000.
Classes
RpcError
class rpc.RpcError < Error
A JSON-RPC error: what a request failed with, on either side of a connection.
Raised from a request handler, it answers the request: its code,
message and data go back to the caller as they are.
Endpoint.request() raises one when the other side answers a request
with an error.
JSON-RPC reserves the codes from -32768 to -32000 for itself; the
constants in this module name the ones it defines. An application’s own
errors take any code outside that range.
import rpc
var error = rpc.RpcError(rpc.INVALID_PARAMS, 'a name is required', { field: 'name' })
echo error.code # -32602
echo error.to_dict() # {code: -32602, message: a name is required, data: {field: name}}
Constructor
rpc.RpcError(code: int, message: string, data)
Returns a new RpcError.
Parameters
code(int) — The error’s code.message(string) — A short description of what went wrong.data(any) — Anything more the other side may need, sent along with the error. Defaultnil, which sends nothing.
RpcError.to_dict()
rpc.RpcError.to_dict() -> dict
The error as JSON-RPC sends it: { code, message }, and data as well
when the error carries any.
Returns dict
RpcFramingError
class rpc.RpcFramingError < Error
Raised when the framing of a stream cannot be read: a header block that is not ASCII, a header without a length, a length that is not a whole number, a header or message larger than the framing allows, or a charset other than UTF-8. Nothing after it in the stream can be read.
Constructor
rpc.RpcFramingError(message: string)
Returns a new RpcFramingError.
Parameters
message(string) — What was wrong with the stream.
RpcClosedError
class rpc.RpcClosedError < Error
Raised by Endpoint.request() when the connection ends before the
answer arrives, and by sending on an endpoint that has been closed.
Constructor
rpc.RpcClosedError(message: string)
Returns a new RpcClosedError.
Parameters
message(string) — What could not be done.
RpcTimeoutError
class rpc.RpcTimeoutError < Error
Raised by Endpoint.request() when its timeout runs out before the
answer arrives.
Constructor
rpc.RpcTimeoutError(message: string)
Returns a new RpcTimeoutError.
Parameters
message(string) — What timed out.
RpcHttpError
class rpc.RpcHttpError < Error
Raised by an HttpClient when the server
answers with an HTTP status that carries no JSON-RPC answer: anything
other than 200, or 202 and 204 for a message that needs no answer,
unless the body is a JSON-RPC answer all the same.
Constructor
rpc.RpcHttpError(status: int, message: string, body: string)
Returns a new RpcHttpError.
Parameters
status(int) — The HTTP status the server answered with.message(string) — What went wrong.body(string) — The body the server sent with it, as text;''for none.
2026, Richard Ore and Zuri contributors
rpc.framing
import rpc
Everything here is re-exported by
rpc, soimport rpcis enough and the names are called asrpc.*. Importingrpc.framingon its own works too and reaches the same definitions.
How messages are told apart in a stream of bytes. A stream carries one
message after another with nothing to mark where one ends, so each is
framed: HeaderFraming puts a header giving its
length in front of it, and LineFraming ends it with a
line break. A transport that already keeps messages apart, as a
WebSocket does, needs no framing of its own, and
MessageFraming takes each of its reads as one whole
message.
A framing is fed the bytes as they arrive, in whatever pieces they arrive in, and hands back each message once the whole of it is there. It frames the other way too, turning a message’s text into the bytes to send.
Re-exported by rpc.
Constants
DEFAULT_MAX_SIZE
rpc.DEFAULT_MAX_SIZE = 67108864
The largest message a framing accepts unless told otherwise: 64 MiB.
Classes
HeaderFraming
class rpc.HeaderFraming
Frames each message with a header block giving its length:
Content-Length: 42\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"status"}
The header block is ASCII: lines of Name: value, each ended by \r\n,
and a blank line after them. Content-Length gives the length of the
message after the blank line, in bytes, and is required. Content-Type
may be given, and if it names a charset that charset must be UTF-8.
Header names are read without regard to case, and any other header is
ignored. A message is sent with a Content-Length header alone.
import rpc
var framing = rpc.HeaderFraming()
var stream = framing.frame('{"a":1}') + framing.frame('{"b":2}')
# However the bytes are split, the same two messages come out.
var first = framing.feed(stream[0, 30])
var second = framing.feed(stream[30, stream.length()])
echo first.map(@(m) => m.to_string()) # [{"a":1}]
echo second.map(@(m) => m.to_string()) # [{"b":2}]
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.HeaderFraming()
Returns a new HeaderFraming with nothing buffered
and a maximum message size of DEFAULT_MAX_SIZE.
HeaderFraming.set_max_size()
rpc.HeaderFraming.set_max_size(size: int) -> HeaderFraming
Sets the largest message, in bytes, that feed() accepts.
Parameters
size(int) — DefaultDEFAULT_MAX_SIZE, 64 MiB.
Returns HeaderFraming — itself.
HeaderFraming.max_size()
rpc.HeaderFraming.max_size() -> int
The largest message, in bytes, that feed() accepts.
Returns int
HeaderFraming.pending()
rpc.HeaderFraming.pending() -> int
How many bytes are held waiting for the rest of a message.
Returns int
HeaderFraming.feed()
rpc.HeaderFraming.feed(data: bytes) -> list[bytes]
Takes the next bytes of the stream and returns every message they
complete, as the bytes of its text, in the order they arrived. A message
cut off at the end of data is held until the rest of it is fed.
Parameters
data(bytes) — The next bytes of the stream.
Returns list[bytes]
Raises RpcFramingError when the header block is not ASCII, has no
Content-Length, or runs past 8 KiB, its length is not a whole number
of bytes, a header line has no :, the charset is not UTF-8, or the
message is larger than max_size(). Nothing after it in the stream can
be read.
HeaderFraming.frame()
rpc.HeaderFraming.frame(message) -> bytes
The bytes that send message: its text with a Content-Length header
in front.
Parameters
message(string|bytes) — The message’s text.
Returns bytes
HeaderFraming.to_string()
rpc.HeaderFraming.to_string() -> string
This framing as HeaderFraming(n bytes pending).
Returns string
LineFraming
class rpc.LineFraming
Frames each message as one line, ended by \n, the framing of
newline-delimited JSON. JSON text holds no line breaks of its own, so a
line is always exactly one message.
A line may end in \r\n as well, and an empty line is skipped.
import rpc
var framing = rpc.LineFraming()
var lines = framing.feed('{"a":1}\n{"b":'.to_bytes())
echo lines.length() # 1
echo framing.feed('2}\n'.to_bytes())[0].to_string() # {"b":2}
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.LineFraming()
Returns a new LineFraming with nothing buffered and a
maximum message size of DEFAULT_MAX_SIZE.
LineFraming.set_max_size()
rpc.LineFraming.set_max_size(size: int) -> LineFraming
Sets the longest line, in bytes, that feed() accepts.
Parameters
size(int) — DefaultDEFAULT_MAX_SIZE, 64 MiB.
Returns LineFraming — itself.
LineFraming.max_size()
rpc.LineFraming.max_size() -> int
The longest line, in bytes, that feed() accepts.
Returns int
LineFraming.pending()
rpc.LineFraming.pending() -> int
How many bytes are held waiting for the end of a line.
Returns int
LineFraming.feed()
rpc.LineFraming.feed(data: bytes) -> list[bytes]
Takes the next bytes of the stream and returns every line they complete,
without its line break, as bytes, in the order they arrived. A line cut
off at the end of data is held until the rest of it is fed.
Parameters
data(bytes) — The next bytes of the stream.
Returns list[bytes]
Raises RpcFramingError when a line runs past max_size(). Nothing
after it in the stream can be read.
LineFraming.frame()
rpc.LineFraming.frame(message) -> bytes
The bytes that send message: its text and a \n.
Parameters
message(string|bytes) — The message’s text, which must hold no line break.
Returns bytes
LineFraming.to_string()
rpc.LineFraming.to_string() -> string
This framing as LineFraming(n bytes pending).
Returns string
MessageFraming
class rpc.MessageFraming
Takes each piece it is fed as one whole message, for a transport that keeps messages apart itself, such as a WebSocket, whose every read returns exactly one message. Framing a message adds nothing to it.
import rpc
var framing = rpc.MessageFraming()
echo framing.feed('{"a":1}'.to_bytes())[0].to_string() # {"a":1}
echo framing.frame('{"b":2}').to_string() # {"b":2}
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.MessageFraming()
Returns a new MessageFraming with a maximum message
size of DEFAULT_MAX_SIZE.
MessageFraming.set_max_size()
rpc.MessageFraming.set_max_size(size: int) -> MessageFraming
Sets the largest message, in bytes, that feed() accepts.
Parameters
size(int) — DefaultDEFAULT_MAX_SIZE, 64 MiB.
Returns MessageFraming — itself.
MessageFraming.max_size()
rpc.MessageFraming.max_size() -> int
The largest message, in bytes, that feed() accepts.
Returns int
MessageFraming.pending()
rpc.MessageFraming.pending() -> int
How many bytes are held waiting for the rest of a message: always 0,
since every piece is a message of its own.
Returns int
MessageFraming.feed()
rpc.MessageFraming.feed(data: bytes) -> list[bytes]
Takes one whole message and returns it, alone in a list. Empty data
holds no message and returns an empty list.
Parameters
data(bytes) — One whole message.
Returns list[bytes]
Raises RpcFramingError when the message is larger than
max_size().
MessageFraming.frame()
rpc.MessageFraming.frame(message) -> bytes
The bytes that send message: its text, as it is.
Parameters
message(string|bytes) — The message’s text.
Returns bytes
MessageFraming.to_string()
rpc.MessageFraming.to_string() -> string
This framing as MessageFraming().
Returns string
2026, Richard Ore and Zuri contributors
rpc.http
import rpc
Everything here is re-exported by
rpc, soimport rpcis enough and the names are called asrpc.*. Importingrpc.httpon its own works too and reaches the same definitions.
JSON-RPC over HTTP, both ends of it. Each message, or batch, is the body
of a POST, and its answer is the body of the response, so every
exchange stands on its own: no connection is kept between calls, and
nothing is framed.
http_handler() serves a Service from a
route of an http server, and HttpClient calls a
JSON-RPC service over HTTP.
The statuses on the wire follow common practice:
| Status | When |
|---|---|
200 | an answer, whether it is a result or a JSON-RPC error |
204 | nothing to answer: a notification, or a batch of them |
405 | a method other than POST |
415 | a body that is not application/json in UTF-8 |
A JSON-RPC error never changes the status: METHOD_NOT_FOUND comes back
as a 200 whose body says so. The http server’s own limits apply
first, so a body over its max_body_size is refused with 413 before
it reaches the service.
Re-exported by rpc.
Functions
http_handler()
rpc.http_handler(service) -> function(2):
A route handler for an http server that answers JSON-RPC with
service: register it for POST on whatever path the service lives at.
import http
import rpc
def setup(server) {
var calculator = rpc.Service()
.on_request('add', @(params) => params[0] + params[1])
server.post('/rpc', rpc.http_handler(calculator))
}
http.serve(setup, { port: 8545 })
Every handler of the service is given the http request in
context.request, for its headers, its client’s address, or what a
middleware put in its context. A request cannot be deferred, since its
answer is the response to the HTTP request it came in.
Registered for every method with server.any(), the handler answers
anything other than POST with 405 and an Allow header, as the
other methods should be answered.
Parameters
service(Service) — What answers the messages.
Returns function(2): — A handler taking an HttpRequest and an
HttpResponse.
Classes
HttpClient
class rpc.HttpClient
Calls a JSON-RPC service over HTTP: each call is one POST to url,
and its answer is the response.
import rpc
var node = rpc.HttpClient('https://node.example.com', {
headers: { Authorization: 'Bearer ${token}' },
timeout: 10,
})
echo node.request('eth_blockNumber', [])
var answers = node.request_batch([
['eth_blockNumber', []],
['eth_gasPrice', []],
])
An answer is matched to its call by id, never by position, so a
batch’s answers come back in the order of its calls whatever order the
server sent them in.
Some servers send a JSON-RPC error with a status other than 200. When
the body of such a response is a JSON-RPC answer, it is read like any
other answer, and its error raises from the call. Any other status
raises an RpcHttpError.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.HttpClient(url: string, options: ?dict)
Returns a new HttpClient calling the service at url.
| Option | Meaning |
|---|---|
headers | headers sent with every call, such as Authorization |
timeout | the most seconds to wait for an answer |
client | the http.HttpClient to send through |
headers defaults to none. timeout defaults to nil, which leaves
the wait to the http client’s own read timeout. client defaults to a
new http.HttpClient; pass one for its proxy, TLS and connection
settings.
Parameters
url(string) — Where the service is, anhttp:orhttps:URL.options(?dict) — Defaultnil, every option at its default.
HttpClient.request()
rpc.HttpClient.request(method: string, params, timeout: ?number)
Calls method and returns its result.
Parameters
method(string)params(list|dict|nil)timeout(?number) — The most seconds to wait for the answer. Defaultnil, the client’s owntimeout.
Returns — any: The result the service answered with.
Raises RpcError when the service answers with an error, or its
answer is not a valid answer to the call.
Raises RpcHttpError when the server answers with an HTTP status
that carries no answer.
HttpClient.notify()
rpc.HttpClient.notify(method: string, params)
Sends a notification: calls method and expects nothing back.
Parameters
method(string)params(list|dict|nil) — Defaultnil, which sends none.
Raises RpcHttpError when the server answers with an HTTP status
that carries no answer.
HttpClient.request_batch()
rpc.HttpClient.request_batch(calls: list, timeout: ?number)
Sends several requests as one batch, in one POST, and returns every
answer in the order of calls.
Parameters
calls(list) — Each call as a pair,[method, params].timeout(?number) — The most seconds to wait for the answer. Defaultnil, the client’s owntimeout.
Returns — list: For each call, in order, the result the service
answered with, or the {RpcError} it answered with. A call the service
left unanswered holds an {RpcError} with code INTERNAL_ERROR saying
so.
Raises ValueError when calls is empty, or a call is not a
[method, params] pair.
Raises RpcError when the service refuses the batch whole, or its
answer is not a batch of answers.
Raises RpcHttpError when the server answers with an HTTP status
that carries no answer.
HttpClient.send()
rpc.HttpClient.send(message, timeout: ?number) -> Response|list|nil
Sends a message as it is, a Request, a
Notification, or a list of them as a
batch, and returns the answer as it came: a
Response, a list of what the batch’s answer
holds, or nil when the server answered with nothing.
Parameters
message(Request|Notification|list)timeout(?number) — The most seconds to wait for the answer. Defaultnil, the client’s owntimeout.
Returns Response|list|nil
Raises RpcError when the answer is not valid JSON-RPC.
Raises RpcHttpError when the server answers with an HTTP status
that carries no answer.
HttpClient.to_string()
rpc.HttpClient.to_string() -> string
This client as HttpClient(url).
Returns string
2026, Richard Ore and Zuri contributors
rpc.message
import rpc
Everything here is re-exported by
rpc, soimport rpcis enough and the names are called asrpc.*. Importingrpc.messageon its own works too and reaches the same definitions.
The messages JSON-RPC 2.0 exchanges, a Request, a
Notification and a Response, and
turning them to and from JSON.
Every message is a JSON object whose jsonrpc member is the string
'2.0'. A request names a method, may carry params, and has an id
its response is matched to. A notification is a request without an id,
and nothing answers it. A response carries the id of the request it
answers and either a result or an error, never both. A message may
also be a batch: a JSON array of messages sent and answered together.
Re-exported by rpc.
Functions
read()
rpc.read(value) -> Request|Notification|Response|list
Reads a value decoded from JSON as a message: a Request, a
Notification or a Response.
A batch, a list, comes back as a list holding, for each of its entries
in order, the message it holds or the
RpcError saying why it does not hold one, so
every entry can be answered on its own. An empty batch is not a message.
Parameters
value(any) — A value asjson.decode()returns it.
Returns Request|Notification|Response|list
Raises RpcError with code INVALID_REQUEST when value is not a
message, or is an empty batch.
decode()
rpc.decode(text: string) -> Request|Notification|Response|list
Decodes the JSON text of a message, or of a batch, and reads it as
read() does.
import rpc
var call = rpc.decode('{"jsonrpc": "2.0", "id": 7, "method": "ping"}')
echo call.method # ping
echo call.id # 7
Parameters
text(string) — The message’s JSON text.
Returns Request|Notification|Response|list
Raises RpcError with code PARSE_ERROR when text is not valid
JSON, or code INVALID_REQUEST when it is not a message.
encode()
rpc.encode(message) -> string
The JSON text of a message, or of a list of messages sent as a batch.
Parameters
message(Request|Notification|Response|list)
Returns string
Classes
Request
class rpc.Request
A call that expects an answer: the method to run, its params, and
the id the answer comes back with.
import rpc
var call = rpc.Request('add', [2, 3], 1)
echo rpc.encode(call) # {"jsonrpc":"2.0","id":1,"method":"add","params":[2,3]}
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.Request(method: string, params, id)
Returns a new Request.
Parameters
method(string) — The method to call.params(list|dict|nil) — Its parameters, by position as a list or by name as a dictionary;nilsends none.id(string|number) — What the answer will be matched by.
Raises ValueError when params is not a list, a dictionary or
nil.
Request.to_dict()
rpc.Request.to_dict() -> dict
The request as the dictionary JSON-RPC sends.
Returns dict
Request.to_string()
rpc.Request.to_string() -> string
This request as Request(method #id).
Returns string
Notification
class rpc.Notification
A call that expects no answer: the method to run and its params.
Nothing comes back from a notification, not even an error.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.Notification(method: string, params)
Returns a new Notification.
Parameters
method(string) — The method to call.params(list|dict|nil) — Its parameters, by position as a list or by name as a dictionary;nilsends none.
Raises ValueError when params is not a list, a dictionary or
nil.
Notification.to_dict()
rpc.Notification.to_dict() -> dict
The notification as the dictionary JSON-RPC sends.
Returns dict
Notification.to_string()
rpc.Notification.to_string() -> string
This notification as Notification(method).
Returns string
Response
class rpc.Response
The answer to a Request: its id, and either the result
the method returned or the error it failed with.
The id is nil only for an answer to a message whose id could not
be read, such as one that was not valid JSON.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.Response(id, result, error)
Returns a new Response. success() and failure() say
which kind more plainly.
Parameters
id(string|number|nil) — Theidof the request answered.result(any) — What the method returned; ignored whenerroris given.error(?RpcError) — What the method failed with, ornilfor a success.
Response.success()
rpc.Response.success(id, result) -> Response
A response answering request id with result.
Parameters
id(string|number)result(any)
Returns Response
Response.failure()
rpc.Response.failure(id, error) -> Response
A response answering request id with error.
Parameters
id(string|number|nil)error(RpcError)
Returns Response
Response.is_error()
rpc.Response.is_error() -> bool
True when the response carries an error rather than a result.
Returns bool
Response.to_dict()
rpc.Response.to_dict() -> dict
The response as the dictionary JSON-RPC sends.
Returns dict
Response.to_string()
rpc.Response.to_string() -> string
This response as Response(#id ok) or Response(#id error code).
Returns string
2026, Richard Ore and Zuri contributors
rpc.server
import rpc
Everything here is re-exported by
rpc, soimport rpcis enough and the names are called asrpc.*. Importingrpc.serveron its own works too and reaches the same definitions.
Serving JSON-RPC to many connections at once, over TCP, TLS or a Unix domain socket.
serve() binds a listening socket and spreads the connections it
accepts across a pool of worker isolates. Every connection gets an
Endpoint of its own, and setup gives it
its handlers, so each connection is a full JSON-RPC peer: it answers
what its client asks, and it can call its client and notify it in
return.
Each worker polls the connections it holds rather than blocking on one, so a quiet client occupies a descriptor and not a thread, and one worker serves many long-lived connections side by side.
Re-exported by rpc.
Functions
serve()
rpc.serve(setup, options: ?dict)
Serves JSON-RPC on a listening socket, across a pool of worker isolates, until it is stopped.
setup is called inside a worker for every connection accepted, with
the connection’s own Endpoint and the
client’s address, and gives the endpoint its handlers. A worker shares
nothing with the others or with this isolate, so setup builds whatever
state a connection needs, and is a function of a module, or a function
that uses nothing but its own imports.
import rpc
def setup(endpoint, peer) {
endpoint
.on_request('add', @(params) => params[0] + params[1])
.on_request('whoami', @() => peer.to_string())
}
rpc.serve(setup, { port: 4444, framing: 'line' })
| Option | Meaning | Default |
|---|---|---|
host | the address to listen on | '127.0.0.1' |
port | the port to listen on; 0 picks a free one | 8000 |
path | a Unix domain socket to listen on, in place of host and port | nil |
workers | how many worker isolates serve connections | the number of CPUs |
backlog | how many accepted connections may wait for a worker | workers * 4 |
framing | 'header' for Content-Length headers, 'line' for lines | 'header' |
max_message_size | the largest message accepted, in bytes | 64 MiB |
max_connections_per_worker | the most connections one worker holds | 256 |
idle_timeout | seconds a connection may stay silent before it is closed | nil, never |
read_timeout | seconds a read may wait once a message has begun | 30 |
write_timeout | seconds a write may wait | 30 |
cert_chain, private_key | PEM strings that put every connection behind TLS | nil |
on_ready | called here once bound, with the address and a stop function | nil |
max_connections | stop after accepting this many connections | nil, never |
on_ready is called on this isolate once the socket is bound, with the
address it is bound to, as a SocketAddr or the socket’s path, and a
function that stops the server. Stopping it stops accepting connections;
every worker then finishes the message it is handling, closes its
connections and ends, and serve() returns. Reaching max_connections
stops accepting too, but serves the connections already accepted until
each of them closes, and serve() returns after the last.
A connection whose TLS handshake fails, or whose setup raises, is
closed and nothing else is affected. A connection that cannot be read is
reported to its endpoint’s on_error() handler and closed.
A handler that calls its client with request() holds its worker until
the answer arrives, and every other connection on that worker waits with
it.
The socket file of a Unix domain socket is removed when the server
stops. A file already at path makes binding fail.
A failure accepting connections stops the server as stop would, and
raises from serve() once it has.
Parameters
setup(function(2)) — Called with each connection’s {Endpoint} and the client’s address.options(?dict) — Defaultnil, every option at its default.
Raises ValueError when an option holds a value it cannot take.
Raises Error when the socket cannot be bound.
2026, Richard Ore and Zuri contributors
rpc.service
import rpc
Everything here is re-exported by
rpc, soimport rpcis enough and the names are called asrpc.*. Importingrpc.serviceon its own works too and reaches the same definitions.
Service: what a program answers. A service holds the
handler for each method the program serves, and answers every message it
is given with them, whatever carried the message there.
A service on its own answers messages that each arrive in an exchange of
their own, such as the body of an HTTP request, and whose answer goes
back in that same exchange. An Endpoint is
a service on a connection: it answers what arrives on the connection,
and calls the other side of it as well.
Re-exported by rpc.
Constants
DEFAULT_BATCH_LIMIT
rpc.DEFAULT_BATCH_LIMIT = 1000
The most messages a batch may hold before it is refused whole, 1000,
until Service.set_batch_limit() says otherwise.
Classes
Context
class rpc.Context
What a handler is told about the message it is handling: the request’s
id, its method, the endpoint handling it, and the request it
arrived in.
A request handler normally answers by returning. One on an
Endpoint that cannot answer before it
returns, because the answer comes from work still to be done, calls
defer(), keeps the context, and answers through it with reply() or
fail() once the answer is ready.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.Context(endpoint, id, method: string, is_request: bool, request, can_defer: bool)
Returns a new Context. A service makes one for each
message it hands to a handler.
Parameters
endpoint(Service) — The {Service} or {Endpoint} handling the message.id(string|number|nil) — The request’sid;nilfor a notification.method(string) — The method called.is_request(bool) — Whether the message is a request, which is answered, rather than a notification, which is not.request(any) — What the message arrived in, such as thehttprequest whose body it was;nilfor a message from a connection.can_defer(bool) — Whether the request can be answered after its handler returns, which needs a connection to answer on.
Context.is_notification()
rpc.Context.is_notification() -> bool
True when the message is a notification, which nothing answers.
Returns bool
Context.defer()
rpc.Context.defer() -> Context
Takes over answering the request: whatever the handler returns is
ignored, and the request is answered when reply() or fail() is
called, from the isolate that owns the endpoint. Calling it again
changes nothing.
Only a request that arrived on an
Endpoint’s connection can be deferred. One
answered in its own exchange, such as an HTTP request, is answered when
its handler returns.
A request deferred from inside a batch is answered on its own, after the batch’s other answers.
Returns Context — itself.
Raises ValueError for a notification, which is never answered, or
for a request with no connection to answer it on later.
Context.is_deferred()
rpc.Context.is_deferred() -> bool
True once defer() has been called.
Returns bool
Context.reply()
rpc.Context.reply(result)
Answers the deferred request with result.
Parameters
result(any)
Raises ValueError when the request was not deferred, or has
already been answered.
Raises RpcClosedError when the endpoint has been closed.
Context.fail()
rpc.Context.fail(error)
Answers the deferred request with error.
Parameters
error(RpcError)
Raises ValueError when the request was not deferred, or has
already been answered.
Raises RpcClosedError when the endpoint has been closed.
Context.is_answered()
rpc.Context.is_answered() -> bool
True once a deferred request has been answered.
Returns bool
Context.to_string()
rpc.Context.to_string() -> string
This context as Context(method #id).
Returns string
Service
class rpc.Service
The methods a program serves, and the answering of every message sent to them.
Handlers say what it answers. A request handler is called with the
request’s params and a Context, and what it returns is
the result sent back; an RpcError it raises
is sent back as that error, and any other error as an INTERNAL_ERROR
carrying its message. A notification handler is called the same way and
nothing is sent back. A message for a method with no handler goes to the
on_unhandled() fallback when one is set; without one, a request is
answered with METHOD_NOT_FOUND and a notification is dropped. A
message that is not valid UTF-8, or not valid JSON, is answered with
PARSE_ERROR.
import rpc
var calculator = rpc.Service()
.on_request('add', @(params) => params[0] + params[1])
echo calculator.answer('{"jsonrpc": "2.0", "id": 1, "method": "add", "params": [2, 3]}')
# {"jsonrpc":"2.0","id":1,"result":5}
answer() is the whole of a service’s work: one message or batch in,
its answer out. rpc.http_handler() puts a service behind an http
route, and an Endpoint puts one on a
connection.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.Service()
Returns a new Service with no handlers and a batch limit
of DEFAULT_BATCH_LIMIT.
Service.on_request()
rpc.Service.on_request(method: string, handler: function) -> Service
Answers requests for method with handler, in place of any handler
set for it before.
Parameters
method(string) — The method’s name. Names starting withrpc.are reserved by JSON-RPC.handler(function(2)) — Called with the request’sparamsand a {Context}; what it returns is the result sent back.
Returns Service — itself.
Raises ValueError for a name starting with rpc..
Service.on_notification()
rpc.Service.on_notification(method: string, handler: function) -> Service
Handles notifications for method with handler, in place of any
handler set for it before.
Parameters
method(string) — The method’s name. Names starting withrpc.are reserved by JSON-RPC.handler(function(2)) — Called with the notification’sparamsand a {Context}; what it returns is ignored.
Returns Service — itself.
Raises ValueError for a name starting with rpc..
Service.on_unhandled()
rpc.Service.on_unhandled(handler: function) -> Service
Handles every request and notification whose method has no handler of
its own, in place of any fallback set before. It is called as the other
handlers are, with context.method naming the method. For a request,
what it returns is the result sent back, and raising an
RpcError with METHOD_NOT_FOUND refuses the
method as a service without a fallback would.
A method whose name starts with rpc. never reaches it: JSON-RPC
reserves those names, so such a request is answered with
METHOD_NOT_FOUND and such a notification is dropped.
Parameters
handler(function(2)) — Called with the message’sparamsand a {Context}.
Returns Service — itself.
Service.on_error()
rpc.Service.on_error(handler: function) -> Service
Called with every problem the other side is not told about: an error a
notification handler raised, an error other than an
RpcError a request handler raised, a response
nothing was waiting for, and, on an
Endpoint, an error a send_request()
callback raised and a connection that failed.
Parameters
handler(function(2)) — Called with the error and the message it concerns, as a dictionary, ornilwhen there is no one message.
Returns Service — itself.
Service.on_trace()
rpc.Service.on_trace(handler: function) -> Service
Called with the text of every message, as it arrives and as it is sent, for logging a conversation.
Parameters
handler(function(2)) — Called with'in'or'out'and the message’s JSON text.
Returns Service — itself.
Service.set_batch_limit()
rpc.Service.set_batch_limit(limit: ?int) -> Service
Sets the most messages a batch may hold. A larger batch is refused whole
with one INVALID_REQUEST error and an id of nil, before any of it
is handled, so one message cannot make a service do an unbounded amount
of work.
Parameters
limit(?int) — DefaultDEFAULT_BATCH_LIMIT,1000.niltakes a batch of any size.
Returns Service — itself.
Raises ValueError for a limit below 1.
Service.batch_limit()
rpc.Service.batch_limit() -> ?int
The most messages a batch may hold, or nil for no limit.
Returns ?int
Service.answer()
rpc.Service.answer(message, request) -> ?string
Answers one message, or a batch of them, and returns the JSON text of
the answer, or nil when there is nothing to answer: a notification, or
a batch of nothing but notifications.
import rpc
var greeter = rpc.Service()
.on_request('greet', @(params) => 'Hello, ${params.name}!')
var call = '{"jsonrpc": "2.0", "id": 7, "method": "greet", "params": {"name": "Ada"}}'
var note = '{"jsonrpc": "2.0", "method": "greet", "params": {"name": "Ada"}}'
echo greeter.answer(call) # {"jsonrpc":"2.0","id":7,"result":"Hello, Ada!"}
echo greeter.answer(note) # nil
Parameters
message(string|bytes) — The message’s JSON text, or the bytes of it, which must be UTF-8.request(any) — What the message arrived in, handed to every handler ascontext.request, such as thehttprequest whose body it was. Defaultnil.
Returns ?string
Service.to_string()
rpc.Service.to_string() -> string
This service as Service(n methods).
Returns string
2026, Richard Ore and Zuri contributors
rpc.transport
import rpc
Everything here is re-exported by
rpc, soimport rpcis enough and the names are called asrpc.*. Importingrpc.transporton its own works too and reaches the same definitions.
Where an endpoint’s bytes come from and go to. A transport is any object with three methods:
read(max)returns the next bytes that arrive, at mostmaxof them, waiting until at least one does. It returns empty bytes once the stream has ended.write(data)sends all ofdata, abytes.close()ends the stream in the direction it writes.
A transport may also have can_listen(), true when another isolate can
read it while the one that made it writes to it, which
Endpoint.listen() relies on. Without the method, it is false. And a
transport that keeps messages apart itself has framing(), returning
the framing an endpoint over it uses unless told otherwise.
StdioTransport talks over the program’s own
standard input and output, SocketTransport over a
connected socket, ProcessTransport over the
standard input and output of a child process,
WebSocketTransport over a WebSocket, and
ChannelTransport over isolate channels, two of
which pipe() joins into a connection between isolates. Any other
object with the three methods works the same.
Re-exported by rpc.
Functions
pipe()
rpc.pipe() -> list[ChannelTransport]:
Two ChannelTransports joined to each other: what
one writes, the other reads. Hand one end to another isolate and the two
can talk JSON-RPC across the isolate boundary.
import rpc
var ends = rpc.pipe()
ends[0].write('ping'.to_bytes())
echo ends[1].read(4096).to_string() # ping
Returns list[ChannelTransport]: — The two ends.
Classes
StdioTransport
class rpc.StdioTransport
Talks over the program’s own standard input and output: what it reads arrives on stdin, and what it writes goes to stdout. This is the transport of a program that another one starts and talks to.
Every write is flushed at once. Anything else the program prints to stdout lands in the middle of the stream, so a program serving over stdio writes everything else to stderr.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.StdioTransport()
Returns a new StdioTransport.
StdioTransport.read()
rpc.StdioTransport.read(max: int) -> bytes
The next bytes on stdin, at most max of them, or empty bytes once
stdin has ended.
Parameters
max(int)
Returns bytes
StdioTransport.write()
rpc.StdioTransport.write(data: bytes)
Writes data to stdout and flushes it.
Parameters
data(bytes)
StdioTransport.close()
rpc.StdioTransport.close()
Flushes stdout. The program’s standard streams stay open.
StdioTransport.can_listen()
rpc.StdioTransport.can_listen() -> bool
True: every isolate reads the same stdin, so one can read it while another writes.
Returns bool
StdioTransport.to_string()
rpc.StdioTransport.to_string() -> string
This transport as StdioTransport().
Returns string
SocketTransport
class rpc.SocketTransport
Talks over a connected stream from net: a TcpStream, a UnixStream
or a TlsStream, or anything else with read(), write_all() and
close().
A socket is held by one isolate at a time, so an endpoint serving one
reads it with serve() rather than listen().
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.SocketTransport(socket)
Returns a new SocketTransport over socket.
Parameters
socket(any) — A connected socket.
SocketTransport.read()
rpc.SocketTransport.read(max: int) -> bytes
The next bytes from the socket, at most max of them, or empty bytes
once the other side has closed its end.
Parameters
max(int)
Returns bytes
Raises Error when the socket fails, or its read timeout runs out.
SocketTransport.write()
rpc.SocketTransport.write(data: bytes)
Writes all of data to the socket.
Parameters
data(bytes)
Raises Error when the socket fails.
SocketTransport.close()
rpc.SocketTransport.close()
Closes the socket.
SocketTransport.can_listen()
rpc.SocketTransport.can_listen() -> bool
False: a socket is held by one isolate at a time.
Returns bool
SocketTransport.to_string()
rpc.SocketTransport.to_string() -> string
This transport as SocketTransport().
Returns string
ProcessTransport
class rpc.ProcessTransport
Talks to a child process over its standard input and output, from
os.spawn() with stdin and stdout both 'pipe': what the child
prints is read, and what is written goes to its stdin.
import os
import rpc
var child = os.spawn('zuri', ['run', 'calculator.zu'], {
stdin: 'pipe',
stdout: 'pipe'
})
var calculator = rpc.endpoint(rpc.process(child))
echo calculator.request('add', [2, 3])
The child’s streams are held by the isolate that spawned it, so an
endpoint talking to it reads with serve() and request() rather than
listen().
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.ProcessTransport(process)
Returns a new ProcessTransport over process.
Parameters
process(os.Process) — A child whose stdin and stdout are pipes.
ProcessTransport.read()
rpc.ProcessTransport.read(max: int) -> bytes
The next bytes the child printed, at most max of them, or empty bytes
once its stdout has ended.
Parameters
max(int)
Returns bytes
ProcessTransport.write()
rpc.ProcessTransport.write(data: bytes)
Writes all of data to the child’s stdin.
Parameters
data(bytes)
ProcessTransport.close()
rpc.ProcessTransport.close()
Closes the child’s stdin, so it reads the end of its input. The child itself keeps running until it exits.
ProcessTransport.can_listen()
rpc.ProcessTransport.can_listen() -> bool
False: a child process is read by the isolate that spawned it.
Returns bool
ProcessTransport.to_string()
rpc.ProcessTransport.to_string() -> string
This transport as ProcessTransport().
Returns string
WebSocketTransport
class rpc.WebSocketTransport
Talks over a WebSocket from http.websocket, on either end of it: one
websocket.accept() returned in a route handler, or one
websocket.connect() opened. Each message is one WebSocket message,
sent as text, so an endpoint over it uses a
MessageFraming.
import http.websocket
import rpc
var node = rpc.endpoint(rpc.websocket(websocket.connect('ws://127.0.0.1:8546')))
echo node.request('eth_blockNumber', [])
A WebSocket is held by one isolate at a time, so an endpoint over one
reads with serve() and request() rather than listen().
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.WebSocketTransport(socket)
Returns a new WebSocketTransport over socket.
Parameters
socket(http.websocket.WebSocket) — An open WebSocket.
WebSocketTransport.read()
rpc.WebSocketTransport.read(max: int) -> bytes
The next message that arrives, as its bytes, or empty bytes once the connection has closed. An empty message is skipped, since it holds no JSON-RPC message.
Parameters
max(int) — Ignored; each read returns one message whole.
Returns bytes
WebSocketTransport.write()
rpc.WebSocketTransport.write(data: bytes)
Sends data as one text message.
Parameters
data(bytes) — One message’s UTF-8 text.
WebSocketTransport.close()
rpc.WebSocketTransport.close()
Closes the WebSocket, with the closing handshake.
WebSocketTransport.can_listen()
rpc.WebSocketTransport.can_listen() -> bool
False: a WebSocket is held by one isolate at a time.
Returns bool
WebSocketTransport.framing()
rpc.WebSocketTransport.framing() -> MessageFraming
A MessageFraming: a WebSocket keeps
messages apart itself.
Returns MessageFraming
WebSocketTransport.to_string()
rpc.WebSocketTransport.to_string() -> string
This transport as WebSocketTransport().
Returns string
ChannelTransport
class rpc.ChannelTransport
Talks over two isolate channels: what it reads arrives on inbox and
what it writes goes to outbox. Channels cross isolates, so a
ChannelTransport can be handed to another isolate
and talk to it from there; pipe() makes the two ends of such a
connection.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
rpc.ChannelTransport(inbox, outbox)
Returns a new ChannelTransport.
Parameters
inbox(isolate.Channel) — Where the bytes it reads arrive.outbox(isolate.Channel) — Where the bytes it writes go.
ChannelTransport.read()
rpc.ChannelTransport.read(max: int) -> bytes
The next bytes sent to this end, whatever their length, or empty bytes once the other end has closed.
Parameters
max(int) — Ignored; each read returns one write from the other end whole.
Returns bytes
ChannelTransport.write()
rpc.ChannelTransport.write(data: bytes)
Sends data to the other end.
Parameters
data(bytes)
Raises RpcClosedError when this end has been closed.
ChannelTransport.close()
rpc.ChannelTransport.close()
Closes this end, so the other end reads the end of its input.
ChannelTransport.can_listen()
rpc.ChannelTransport.can_listen() -> bool
True: channels are shared between isolates, so one can read this end while another writes to it.
Returns bool
ChannelTransport.to_string()
rpc.ChannelTransport.to_string() -> string
This transport as ChannelTransport().
Returns string
2026, Richard Ore and Zuri contributors
crypto
import crypto
Comprehensive cryptographic primitives for Zuri applications.
This module exposes symmetric encryption, authenticated encryption, asymmetric key operations, digital signatures, key exchange, and password hashing through a clean, opinionated API.
Algorithm summary
| Family | Algorithms |
|---|---|
| Symmetric | AES-128/192/256-GCM, AES-128/192/256-CBC |
| Stream AEAD | ChaCha20-Poly1305 |
| Asymmetric | RSA-2048/4096 (OAEP + PSS-SHA256/384/512) |
| Signatures | ECDSA P-256/P-384, Ed25519 |
| Key exchange | X25519 (Diffie-Hellman) |
| Password KDF | Argon2id |
| Key stretch | HKDF-SHA256 |
| Key import | jwk.to_pem(): RSA/EC/OKP JWK → PEM |
| Randomness | OS CSPRNG |
Choosing the right primitive
Encrypting data at rest or in transit: prefer aes_gcm
(authenticated encryption with associated data). Use chacha20 on
platforms where AES hardware acceleration is absent. Avoid aes_cbc in
new designs; it is included for interoperability with legacy systems.
Signing and verifying messages: use ed25519 for modern systems
(fastest, no hash to choose, deterministic). Use ecdsa when
interoperating with systems that require NIST curves. Use rsa only
when constrained by a protocol that demands it.
Asymmetric encryption: use rsa with OAEP. For larger payloads, use
a hybrid scheme: exchange a symmetric key with RSA-OAEP, then encrypt
the payload with aes_gcm.
Key agreement: use x25519_exchange followed by hkdf to derive a
symmetric key from the shared secret.
Password storage: always use argon2_hash. Never use raw hashes
(SHA-*, MD5) to store passwords.
Quick start
import crypto
# --- Symmetric (AES-GCM) -----------------------------------------------
var key = crypto.random_bytes(32) # AES-256
var iv = crypto.random_bytes(12) # 96-bit GCM nonce
var ct = crypto.aes_gcm.encrypt(key, iv, bytes('hello world'))
var pt = crypto.aes_gcm.decrypt(key, iv, ct)
echo string(pt) # hello world
# --- Password hashing (Argon2id) ----------------------------------------
var salt = crypto.random_bytes(16)
var hash = crypto.argon2.hash('my password', salt)
echo crypto.argon2.verify(hash, 'my password') # true
# --- Ed25519 signature --------------------------------------------------
var kp = crypto.ed25519.generate()
var sig = crypto.ed25519.sign(kp.private_pem, bytes('payload'))
echo crypto.ed25519.verify(kp.public_pem, bytes('payload'), sig) # true
The crypto API
Every public name in crypto, wherever it is declared. Each links to
the page that documents it.
| Name | Kind | Summary |
|---|---|---|
crypto.CryptoError | class | Raised by crypto operations that fail due to invalid input, key material errors, or authentication failures. |
crypto.aes_cbc | constant | AES-CBC encryption namespace. |
crypto.aes_gcm | constant | AES-GCM authenticated encryption namespace. |
crypto.argon2 | constant | Argon2id password hashing namespace. |
crypto.chacha20 | constant | ChaCha20-Poly1305 AEAD namespace. |
crypto.ecdsa | constant | ECDSA signing namespace. |
crypto.ed25519 | constant | Ed25519 signing namespace. |
crypto.hkdf | function | Derives cryptographic key material from a high-entropy input using HKDF-SHA256 (RFC 5869). |
crypto.jwk | constant | JWK-to-PEM conversion namespace. |
crypto.random_bytes | function | Returns n cryptographically secure random bytes from the operating system’s entropy source (OpenSSL… |
crypto.rsa | constant | RSA encryption and signing namespace. |
crypto.x25519 | constant | X25519 key exchange namespace. |
Constants
aes_gcm
crypto.aes_gcm
AES-GCM authenticated encryption namespace.
See also: _AesGcm
aes_cbc
crypto.aes_cbc
AES-CBC encryption namespace.
See also: _AesCbc
chacha20
crypto.chacha20
ChaCha20-Poly1305 AEAD namespace.
See also: _ChaCha20
rsa
crypto.rsa
RSA encryption and signing namespace.
See also: _Rsa
ecdsa
crypto.ecdsa
ECDSA signing namespace.
See also: _Ecdsa
ed25519
crypto.ed25519
Ed25519 signing namespace.
See also: _Ed25519
x25519
crypto.x25519
X25519 key exchange namespace.
See also: _X25519
jwk
crypto.jwk
JWK-to-PEM conversion namespace.
See also: _Jwk
argon2
crypto.argon2
Argon2id password hashing namespace.
See also: _Argon2
Functions
random_bytes()
crypto.random_bytes(n) -> bytes
Returns n cryptographically secure random bytes from the operating system’s entropy source (OpenSSL RAND_bytes, which seeds from /dev/urandom or the OS equivalent).
Use this to generate keys, IVs, nonces, and salts. Never use
math.random for these purposes.
var key = crypto.random_bytes(32) # 256-bit AES key
var iv = crypto.random_bytes(12) # 96-bit GCM nonce
Parameters
n(number) — Number of bytes to generate. Must be between 1 and 65536.
Returns bytes
Raises CryptoError
hkdf()
crypto.hkdf(ikm, salt, info, length) -> bytes
Derives cryptographic key material from a high-entropy input using HKDF-SHA256 (RFC 5869).
HKDF is not a password hashing function: it does not strengthen
low-entropy inputs. Use argon2 for passwords. HKDF is appropriate for:
- Expanding X25519 shared secrets into symmetric keys. - Deriving multiple purpose-specific sub-keys from a single master key. - Stretching random key material to the required length.
The info parameter binds the derived key to a specific context and
prevents key reuse across different purposes. It does not need to be
secret.
Example
import crypto
# Derive an AES-256 encryption key and a separate HMAC key from a
# shared secret, binding each to its intended purpose.
var secret = crypto.x25519.exchange(my_priv, peer_pub)
var salt = crypto.random_bytes(32)
var enc_key = crypto.hkdf(secret, salt, bytes('encryption'), 32)
var hmac_key = crypto.hkdf(secret, salt, bytes('authentication'), 32)
Parameters
ikm(bytes) — Input key material (high entropy required).salt(bytes) — Random salt. Userandom_bytes(32)or a fixed well-known value if no random salt is available.info(bytes) — Context string binding the key to its purpose.length(number) — Number of output bytes. Maximum 8160.
Returns bytes
Raises CryptoError
Classes
CryptoError
class crypto.CryptoError < Error
Raised by crypto operations that fail due to invalid input, key material errors, or authentication failures. Always catch this class when decrypting untrusted data.
Constructor
crypto.CryptoError(message)
Parameters
message(string)
2024, Zuri Project
hash
import hash
This module provides a framework for cryptographic and non-cryptographic encryption.
Examples,
%> import hash
%>
%> hash.md5('Hello, World')
'82bb413746aee42f89dea2b59614f9ef'
%>
%> hash.sha256('Hello, World')
'03675ac53ff9cd1535ccc7dfcdfa2c458c5218371f418dc136f2d19ac1fbe8a5'
%>
%> hash.hmac_sha256('mykey', 'Hello, World')
'61035d3d2119ffdfd710913bf4161d5fba1c2d9431f7de7ef398d359eb1d2481'
%>
%> hash.hmac_sha256(bytes([10, 11, 12]), 'My secure text!')
'd782079145a3476fd4e018d44dd024034fa91f626f7f30f2009200c5ac757723'
The hash API
Every public name in hash, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
hash.blake2b512 | function | Returns the BLAKE2B-512 cryptographic hash of the given string or bytes. |
hash.blake2s256 | function | Returns the BLAKE2S-256 cryptographic hash of the given string or bytes. |
hash.fnv1 | function | Returns the 32 bit fnv1 hash of the given string or bytes. |
hash.fnv1_64 | function | Returns the 64 bit fnv1 hash of the given string or bytes. |
hash.fnv1a | function | Returns the 32 bit fnv1a hash of the given string or bytes. |
hash.fnv1a_64 | function | Returns the 64 bit fnv1a hash of the given string or bytes. |
hash.gost | function | Returns the Gost cryptographic hash of the given string or bytes. |
hash.hash | function | Returns the hash digest for the given data using the given algorithm. |
hash.hmac | function | Computes an HMAC with the key and str using the given method. |
hash.hmac_gost | function | Returns the HMAC-GOST cryptographic hash of the given string or bytes. |
hash.hmac_md4 | function | Returns the HMAC-MD4 cryptographic hash of the given string or bytes. |
hash.hmac_md5 | function | Returns the HMAC-MD5 cryptographic hash of the given string or bytes. |
hash.hmac_sha1 | function | Returns the HMAC-SHA1 cryptographic hash of the given string or bytes. |
hash.hmac_sha224 | function | Returns the HMAC-SHA224 cryptographic hash of the given string or bytes. |
hash.hmac_sha256 | function | Returns the HMAC-SHA256 cryptographic hash of the given string or bytes. |
hash.hmac_sha384 | function | Returns the HMAC-SHA384 cryptographic hash of the given string or bytes. |
hash.hmac_sha512 | function | Returns the HMAC-SHA512 cryptographic hash of the given string or bytes. |
hash.hmac_whirlpool | function | Returns the HMAC-WHIRLPOOL cryptographic hash of the given string or bytes. |
hash.id | function | Returns the identification hash of a value as used in the underlying dictionary implementation. |
hash.md4 | function | Returns the md4 hash of the given string or bytes. |
hash.md5 | function | Returns the md5 hash of the given string or bytes. |
hash.md5_file | function | Returns the md5 hash of the given file. |
hash.pbkdf2 | function | Derives a cryptographic key from a password using the PBKDF2 key derivation function defined in RFC 2898 §5.2… |
hash.ripemd160 | function | Returns the RIPEMD-160 cryptographic hash of the given string or bytes. |
hash.sha1 | function | Returns the sha1 hash of the given string or bytes. |
hash.sha224 | function | Returns the sha224 hash of the given string or bytes. |
hash.sha256 | function | Returns the sha256 hash of the given string or bytes. |
hash.sha384 | function | Returns the sha384 hash of the given string or bytes. |
hash.sha3_224 | function | Returns the SHA3-224 cryptographic hash of the given string or bytes. |
hash.sha3_256 | function | Returns the SHA3-256 cryptographic hash of the given string or bytes. |
hash.sha3_384 | function | Returns the SHA3-384 cryptographic hash of the given string or bytes. |
hash.sha3_512 | function | Returns the SHA3-512 cryptographic hash of the given string or bytes. |
hash.sha512 | function | Returns the sha512 hash of the given string or bytes. |
hash.shake128 | function | Returns the SHAKE-128 cryptographic hash of the given string or bytes. |
hash.shake256 | function | Returns the SHAKE-256 cryptographic hash of the given string or bytes. |
hash.whirlpool | function | Returns the whirlpool hash of the given string or bytes. |
Functions
id()
hash.id(value) -> number
Returns the identification hash of a value as used in the underlying dictionary implementation.
A class may override the result of this function by implementing the
to_hash decorator.
Parameters
value(any)
Returns number
hash()
hash.hash(algorithm, data, as_bytes) -> string|bytes
Returns the hash digest for the given data using the given algorithm.
Supported algorithms includes:
- FNV1 family:
fnv1,fnv1a,fnv164,fnv1a64. - MD family:md2,md4,md5. - SHA family:sha,sha1,sha224,sha256,sha384,sha512,sha512-224,sha512-256,md5-sha1. - SHA3 family:sha3-224,sha3-256,sha3-384,sha3-512. - SHAKE family (XOF):shake128,shake256. - RIPEMD family:ripemd160. - WHIRLPOOL family:whirlpool. - Blake family:blake2s256,blake2b512. - SM family:sm3.
By default, this function returns the hexadecimal string representing
the hash (since this is the most common application level usage). The
function accepts a third boolean argument as_bytes which allows
callers to specify if the result should be returned in the raw digest
byte stream or not.
Parameters
algorithm(string)data(string|bytes)as_bytes(?bool)
Returns string|bytes
Note: Algorithm names are not case-sensitive.
md4()
hash.md4(str, as_bytes) -> string|bytes
Returns the md4 hash of the given string or bytes.
Parameters
str(string|bytes)as_bytes(?bool)
Returns string|bytes
md5()
hash.md5(str, as_bytes) -> string|bytes
Returns the md5 hash of the given string or bytes.
Parameters
str(string|bytes)as_bytes(?bool)
Returns string|bytes
md5_file()
hash.md5_file(f, as_bytes) -> string|bytes
Returns the md5 hash of the given file.
Parameters
file(file)as_bytes(?bool)
Returns string|bytes
sha1()
hash.sha1(str, as_bytes) -> string|bytes
Returns the sha1 hash of the given string or bytes.
Parameters
str(string|bytes)as_bytes(?bool)
Returns string|bytes
sha224()
hash.sha224(str, as_bytes) -> string|bytes
Returns the sha224 hash of the given string or bytes.
Parameters
str(string|bytes)as_bytes(?bool)
Returns string|bytes
sha256()
hash.sha256(str, as_bytes) -> string|bytes
Returns the sha256 hash of the given string or bytes.
Parameters
str(string|bytes)as_bytes(?bool)
Returns string|bytes
sha384()
hash.sha384(str, as_bytes) -> string|bytes
Returns the sha384 hash of the given string or bytes.
Parameters
str(string|bytes)as_bytes(?bool)
Returns string|bytes
sha512()
hash.sha512(str, as_bytes) -> string|bytes
Returns the sha512 hash of the given string or bytes.
Parameters
str(string|bytes)as_bytes(?bool)
Returns string|bytes
fnv1()
hash.fnv1(data, as_bytes) -> string|bytes
Returns the 32 bit fnv1 hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
fnv1_64()
hash.fnv1_64(data, as_bytes) -> string|bytes
Returns the 64 bit fnv1 hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
fnv1a()
hash.fnv1a(data, as_bytes) -> string|bytes
Returns the 32 bit fnv1a hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
fnv1a_64()
hash.fnv1a_64(data, as_bytes) -> string|bytes
Returns the 64 bit fnv1a hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
whirlpool()
hash.whirlpool(str, as_bytes) -> string|bytes
Returns the whirlpool hash of the given string or bytes.
Parameters
str(string|bytes)as_bytes(?bool)
Returns string|bytes
gost()
hash.gost(data, as_bytes) -> string|bytes
Returns the Gost cryptographic hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
sha3_224()
hash.sha3_224(data, as_bytes) -> string|bytes
Returns the SHA3-224 cryptographic hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
sha3_256()
hash.sha3_256(data, as_bytes) -> string|bytes
Returns the SHA3-256 cryptographic hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
sha3_384()
hash.sha3_384(data, as_bytes) -> string|bytes
Returns the SHA3-384 cryptographic hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
sha3_512()
hash.sha3_512(data, as_bytes) -> string|bytes
Returns the SHA3-512 cryptographic hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
shake128()
hash.shake128(data, as_bytes) -> string|bytes
Returns the SHAKE-128 cryptographic hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
shake256()
hash.shake256(data, as_bytes) -> string|bytes
Returns the SHAKE-256 cryptographic hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
blake2b512()
hash.blake2b512(data, as_bytes) -> string|bytes
Returns the BLAKE2B-512 cryptographic hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
blake2s256()
hash.blake2s256(data, as_bytes) -> string|bytes
Returns the BLAKE2S-256 cryptographic hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
ripemd160()
hash.ripemd160(data, as_bytes) -> string|bytes
Returns the RIPEMD-160 cryptographic hash of the given string or bytes.
Parameters
data(string|bytes)as_bytes(?bool)
Returns string|bytes
hmac()
hash.hmac(method, key, str, as_bytes) -> string|bytes
Computes an HMAC with the key and str using the given method.
Parameters
method(function)key(string|bytes)str(string|bytes)as_bytes(?bool)
Returns string|bytes
hmac_md4()
hash.hmac_md4(key, str, as_bytes) -> string|bytes
Returns the HMAC-MD4 cryptographic hash of the given string or bytes.
Parameters
key(string|bytes)str(string|bytes)as_bytes(?bool)
Returns string|bytes
hmac_md5()
hash.hmac_md5(key, str, as_bytes) -> string|bytes
Returns the HMAC-MD5 cryptographic hash of the given string or bytes.
Parameters
key(string|bytes)str(string|bytes)as_bytes(?bool)
Returns string|bytes
hmac_sha1()
hash.hmac_sha1(key, str, as_bytes) -> string|bytes
Returns the HMAC-SHA1 cryptographic hash of the given string or bytes.
Parameters
key(string|bytes)str(string|bytes)as_bytes(?bool)
Returns string|bytes
hmac_sha224()
hash.hmac_sha224(key, str, as_bytes) -> string|bytes
Returns the HMAC-SHA224 cryptographic hash of the given string or bytes.
Parameters
key(string|bytes)str(string|bytes)as_bytes(?bool)
Returns string|bytes
hmac_sha256()
hash.hmac_sha256(key, str, as_bytes) -> string|bytes
Returns the HMAC-SHA256 cryptographic hash of the given string or bytes.
Parameters
key(string|bytes)str(string|bytes)as_bytes(?bool)
Returns string|bytes
hmac_sha384()
hash.hmac_sha384(key, str, as_bytes) -> string|bytes
Returns the HMAC-SHA384 cryptographic hash of the given string or bytes.
Parameters
key(string|bytes)str(string|bytes)as_bytes(?bool)
Returns string|bytes
hmac_sha512()
hash.hmac_sha512(key, str, as_bytes) -> string|bytes
Returns the HMAC-SHA512 cryptographic hash of the given string or bytes.
Parameters
key(string|bytes)str(string|bytes)as_bytes(?bool)
Returns string|bytes
hmac_whirlpool()
hash.hmac_whirlpool(key, str, as_bytes) -> string|bytes
Returns the HMAC-WHIRLPOOL cryptographic hash of the given string or bytes.
Parameters
key(string|bytes)str(string|bytes)as_bytes(?bool)
Returns string|bytes
hmac_gost()
hash.hmac_gost(key, str, as_bytes) -> string|bytes
Returns the HMAC-GOST cryptographic hash of the given string or bytes.
Parameters
key(string|bytes)str(string|bytes)as_bytes(?bool)
Returns string|bytes
pbkdf2()
hash.pbkdf2(algorithm, password, salt, iterations, dk_len, as_bytes) -> string
Derives a cryptographic key from a password using the PBKDF2 key derivation function defined in RFC 2898 §5.2 (PKCS #5 v2.0), as updated by RFC 8018.
Examples
Password storage (derive then verify)
import hash
var salt = 'f3a8c2b104d7e569' # 16 random bytes in production
var dk = hash.pbkdf2('sha256', 'correct horse battery staple', salt, 600000)
# → 64 lowercase hex characters (32 bytes)
# Verification: re-derive and compare.
if hash.pbkdf2('sha256', candidate, salt, 600000) == dk {
echo 'Password correct'
}
Raw-bytes key for symmetric encryption
import hash
# 32-byte key for AES-256, returned as a bytes object.
var key = hash.pbkdf2('sha256', passphrase, salt, 600000, 32, true)
Longer key with SHA-512
import hash
# 64 bytes; dk_len == hLen so only one T block is needed.
var dk = hash.pbkdf2('sha512', password, salt, 210000, 64)
echo dk.length() # 128 hex characters = 64 bytes
Key that spans multiple PRF blocks
import hash
# 40 bytes with SHA-1 (hLen = 20) requires two T blocks.
var dk = hash.pbkdf2('sha1', 'secret', 'nacl', 4096, 40)
echo dk.length() # 80 hex characters = 40 bytes
Security notes
- Always use a unique, randomly generated salt for every password. Never derive the salt from the username, email, or any other predictable input. - Tune
iterationsso that derivation takes ~100 ms on your target hardware. Re-benchmark as server capacity increases over time. - For pure password storage where output size is not a concern, considerbcrypt(built into Zuri’shashmodule). PBKDF2 is most appropriate when you need an arbitrarily long output : symmetric keys, key wrapping, or protocol key schedules. - When comparing derived keys, use a constant-time equality function to prevent timing side-channel attacks.
Parameters
algorithm(string) — HMAC variant used as the PRF. One of:'sha1','sha224','sha256','sha384','sha512','md5'. Prefer'sha256'or'sha512'for new designs.password(string|bytes) — The password (HMAC key). Any string or bytes value is accepted.salt(string|bytes) — The cryptographic salt. Use at least 16 bytes of random data per password. Never reuse a salt across different passwords.iterations(number) — Iteration countc(must be >= 1). OWASP 2023 minimums: ‘sha1’ → 1 300 000 ‘sha256’ → 600 000 ‘sha512’ → 210 000dk_len(number) — Derived key length in bytes. Defaults to the PRF output length (hLen) when nil or omitted. Maximum: (2^32 - 1) * hLen.as_bytes(bool) — true → return a bytes object. false → return a lowercase hex string (default).
Returns string — | bytes The derived key.
Raises Error
2021, Richard Ore and Zuri contributors
bcrypt
import bcrypt
Generating and verifying bcrypt password hashes, and reading information back out of an existing hash (its cost factor and salt).
hash() and compare() are the two functions almost every caller
needs: hash a password before storing it, then compare a login attempt
against the stored hash later. The salt is generated internally by a
real cryptographically secure random source, embedded in the returned
hash string, and never needs to be handled separately.
Example,
%> import bcrypt
%> var stored = bcrypt.hash('correct horse battery staple')
%> stored
'$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqmVwj0dJmwB3vk...'
%> bcrypt.compare('correct horse battery staple', stored)
true
%> bcrypt.compare('wrong password', stored)
false
The bcrypt API
Every public name in bcrypt, wherever it is declared. Each links to
the page that documents it.
| Name | Kind | Summary |
|---|---|---|
bcrypt.DEFAULT_LOG2_ROUNDS | constant | Default cost factor used by hash() when rounds isn’t given. |
bcrypt.compare | function | Checks whether str is the password known_hash was generated from. |
bcrypt.get_rounds | function | Reads the cost factor a hash was generated with back out of it. |
bcrypt.get_salt | function | Reads the salt portion out of a hash: the first 29 characters, covering the $2x$rounds$salt prefix. |
bcrypt.hash | function | Hashes str with bcrypt, generating a fresh random salt internally on every call: hashing the same string… |
bcrypt.needs_rehash | function | Checks whether a stored hash was generated with a lower cost factor than target_rounds, meaning it should… |
Constants
DEFAULT_LOG2_ROUNDS
bcrypt.DEFAULT_LOG2_ROUNDS: number = 10
Default cost factor used by hash() when rounds isn’t given. Higher
means slower to compute (and slower to brute-force): each increment
roughly doubles the work. 10 is a reasonable default for an
interactive login flow; raise it over time as hardware gets faster (see
needs_rehash()).
Functions
hash()
bcrypt.hash(str: string, rounds: ?number) -> string
Hashes str with bcrypt, generating a fresh random salt internally on
every call: hashing the same string twice always produces two different
(but equally valid) hashes.
Parameters
str(string)rounds(?number) — The cost factor,4-31. DefaultDEFAULT_LOG2_ROUNDS.
Returns string
Raises Error if rounds is out of range.
compare()
bcrypt.compare(str: string, known_hash: string)
Checks whether str is the password known_hash was generated from.
Parameters
str(string)known_hash(string)
Returns — bool: false for a wrong password, and also false
(rather than raising) for a known_hash that isn’t a well-formed bcrypt
hash at all.
get_rounds()
bcrypt.get_rounds(hash: string) -> number
Reads the cost factor a hash was generated with back out of it.
Parameters
hash(string)
Returns number
Raises Error if hash isn’t a well-formed bcrypt hash.
get_salt()
bcrypt.get_salt(hash: string) -> string
Reads the salt portion out of a hash: the first 29 characters, covering
the $2x$rounds$salt prefix.
Parameters
hash(string)
Returns string
Raises Error if hash is shorter than a real bcrypt hash.
Note: this does not validate that
hashis actually a well-formed bcrypt hash; callget_rounds()first if you need that checked.
needs_rehash()
bcrypt.needs_rehash(hash: string, target_rounds: number) -> bool
Checks whether a stored hash was generated with a lower cost factor than
target_rounds, meaning it should be re-hashed (at the user’s next
successful login, typically) to bring it up to the current standard.
Example,
%> if bcrypt.compare(password, user.password_hash) {
.. if bcrypt.needs_rehash(user.password_hash, 12) {
.. user.password_hash = bcrypt.hash(password, 12)
.. }
.. # ... proceed with login
.. }
Parameters
hash(string)target_rounds(number)
Returns bool
2022, Richard Ore and The Zuri Contributors
jwt
import jwt
The jwt module provides a complete implementation of JSON Web Tokens (JWT) as defined in RFC 7519, with support for signing, verification, and inspection of tokens using HMAC, RSA-PSS, ECDSA, and Ed25519 algorithm families.
Supported algorithms:
- HS256/HS384/HS512: HMAC with SHA-256/384/512
- PS256/PS384/PS512: RSASSA-PSS with SHA-256/384/512. This module deliberately supports PS-family, not RS-family (RSASSA-PKCS1-v1_5) algorithms, because they’re older, deterministic standard that lacks a formal security proof and because they’re mostly deterministic while the RSASSA-PSS are probabilistic.
Also, while PKCS#1 v1.5 is simpler to set up, but its structural rigidity has led to historical implementation flaws in encryption (like Bleichenbacher attacks), making PSS cleaner and safer against misuse.
- ES256: ECDSA P-256 with SHA-256
- ES384: ECDSA P-384 with SHA-384
- EdDSA: Ed25519 (RFC 8037)
- none: Unsecured token (no signature, explicit opt-in required)
Signature verification uses a constant-time comparison for the HMAC algorithms (to prevent timing attacks against the shared secret) and the underlying public-key verification routine for every asymmetric algorithm.
Quick start
import jwt
# sign a token
var token = jwt.sign({ user_id: 42, role: 'admin' }, 'secret')
# verify and decode the token
var payload = jwt.verify(token, 'secret')
echo payload.user_id # 42
For applications verifying many tokens with the same configuration,
prefer a Verifier (see [[jwt.Verifier]]) over repeating options on
every verify() call. Likewise, prefer a Signer (see [[jwt.Signer]])
for repeated signing.
When keys are resolved by kid from a JSON Web Key Set rather than
fixed ahead of time, see [[jwt.Jwks]] and [[jwt.verify_with_jwks]].
The jwt API
Every public name in jwt, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
jwt.AlgorithmError | class | Raised when an algorithm specified in a token header is not supported by this module, when the caller… |
jwt.ClaimError | class | Raised when a required claim is absent from the token payload, or when a registered claim (iss, aud, sub)… |
jwt.EDDSA | constant | Ed25519 (EdDSA, RFC 8037) signing algorithm identifier. |
jwt.ES256 | constant | ECDSA P-256 with SHA-256 signing algorithm identifier. |
jwt.ES384 | constant | ECDSA P-384 with SHA-384 signing algorithm identifier. |
jwt.HS256 | constant | HMAC-SHA256 signing algorithm identifier. |
jwt.HS384 | constant | HMAC-SHA384 signing algorithm identifier. |
jwt.HS512 | constant | HMAC-SHA512 signing algorithm identifier. |
jwt.Jwks | class | Parses a JSON Web Key Set document (a dict shaped like { keys: [...] }, exactly what json.decode() on a… |
jwt.JwtError | class | Base error class for all errors raised by the jwt module. |
jwt.MalformedTokenError | class | Raised when a token cannot be decoded because its structure is not valid. |
jwt.NONE | constant | Unsecured algorithm identifier. |
jwt.PS256 | constant | RSA-PSS-SHA256 signing algorithm identifier. |
jwt.PS384 | constant | RSA-PSS-SHA384 signing algorithm identifier. |
jwt.PS512 | constant | RSA-PSS-SHA512 signing algorithm identifier. |
jwt.SignatureError | class | Raised when a token’s signature does not match its header and payload. |
jwt.Signer | class | A reusable token signer that holds a fixed secret and set of default signing options. |
jwt.Token | class | Represents a decoded JWT. |
jwt.TokenExpiredError | class | Raised when a token’s temporal claims fail validation: |
jwt.Verifier | class | A reusable token verifier that holds a fixed secret and set of options. |
jwt.decode | function | Decodes a JWT string into a Token instance without performing signature verification or claim validation. |
jwt.encode | function | Encodes a header dictionary and payload dictionary into a signed JWT string. |
jwt.expires_in | function | Returns the number of seconds until the token expires, based on its exp claim and the current clock. |
jwt.header | function | Returns the decoded header dictionary of a token without verifying its signature. |
jwt.is_expired | function | Returns true when the token’s exp claim is in the past, without verifying the signature. |
jwt.payload | function | Returns the decoded payload dictionary of a token without verifying its signature. |
jwt.sign | function | Creates and signs a new JWT from the given payload. |
jwt.verify | function | Verifies a JWT string and returns its payload (or the full Token when options.complete is true). |
jwt.verify_with_jwks | function | Verifies a token by resolving its signing key from a JWKS document rather than a single fixed secret. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
jwt.codec | jwt.* | Algorithm identifiers and the low-level encoding helpers core.zu builds encode()/verify() out of:… |
jwt.core | jwt.* | The core encode/decode/sign/verify functions the rest of the jwt module is built on. |
jwt.errors | jwt.* | The full error hierarchy raised by the jwt module. |
jwt.jwks | jwt.* | Jwks, plus verify_with_jwks(): resolving a token’s signing key from a JSON Web Key Set (RFC 7517) by its… |
jwt.signer | jwt.* | Signer, a reusable configured wrapper around core.sign(). |
jwt.token | jwt.* | The Token class returned by decode() and verify() (when options.complete is set). |
jwt.verifier | jwt.* | Verifier, a reusable configured wrapper around core.verify(). |
2026, Richard Ore and The Zuri Contributors
jwt.codec
import jwt
Everything here is re-exported by
jwt, soimport jwtis enough and the names are called asjwt.*. Importingjwt.codecon its own works too and reaches the same definitions.
Algorithm identifiers and the low-level encoding helpers core.zu
builds encode()/verify() out of: base64url, HMAC dispatch, a
constant-time comparison, and the DER <-> raw-R‖S conversion ECDSA
signatures need to become JOSE-shaped.
Constants
HS256
jwt.HS256 = 'HS256'
HMAC-SHA256 signing algorithm identifier.
HS384
jwt.HS384 = 'HS384'
HMAC-SHA384 signing algorithm identifier.
HS512
jwt.HS512 = 'HS512'
HMAC-SHA512 signing algorithm identifier.
PS256
jwt.PS256 = 'PS256'
RSA-PSS-SHA256 signing algorithm identifier.
This is deliberately PS256, not RS256: this module’s RSA backing
(crypto.rsa) is RSASSA-PSS, not RSASSA-PKCS1-v1_5, and
PS256/PS384/PS512 are the JOSE-registered names (RFC 7518 §3.5)
for exactly that scheme.
PS384
jwt.PS384 = 'PS384'
RSA-PSS-SHA384 signing algorithm identifier.
PS512
jwt.PS512 = 'PS512'
RSA-PSS-SHA512 signing algorithm identifier.
ES256
jwt.ES256 = 'ES256'
ECDSA P-256 with SHA-256 signing algorithm identifier.
ES384
jwt.ES384 = 'ES384'
ECDSA P-384 with SHA-384 signing algorithm identifier.
EDDSA
jwt.EDDSA = 'EdDSA'
Ed25519 (EdDSA, RFC 8037) signing algorithm identifier.
NONE
jwt.NONE = 'none'
Unsecured algorithm identifier. Tokens signed with this algorithm carry no signature and must never be used to protect sensitive resources. Passing this value to sign() requires setting allow_none: true in options or an error will be raised.
jwt.core
import jwt
Everything here is re-exported by
jwt, soimport jwtis enough and the names are called asjwt.*. Importingjwt.coreon its own works too and reaches the same definitions.
The core encode/decode/sign/verify functions the rest of the jwt
module is built on. Verifier/Signer/Jwks are thin, stateful
wrappers around verify()/sign() defined here.
Functions
encode()
jwt.encode(header, payload, secret, options) -> string
Encodes a header dictionary and payload dictionary into a signed JWT string.
This is the low-level encoding function. For most use cases, sign() is more convenient. Use encode() when you need full control over the header, such as when adding custom header parameters (kid, x5t, etc.).
The secret must be a string for HMAC algorithms, a PEM-encoded RSA private key for PS256/384/512, a PEM-encoded EC private key for ES256/384, or a PEM-encoded Ed25519 private key for EdDSA.
Parameters
header(dict) — must contain at minimum an ‘alg’ keypayload(dict)secret(string)options(?dict) — seesign()for supported option keys
Returns string
Raises AlgorithmError
Raises JwtError
decode()
jwt.decode(token) -> Token
Decodes a JWT string into a Token instance without performing signature verification or claim validation.
This function is useful for inspecting a token’s header or payload
before deciding how to verify it (for example, to read the kid header
parameter and look up the appropriate public key). It must not be used
as the sole step when authenticating a request.
Parameters
token(string)
Returns Token
Raises MalformedTokenError
sign()
jwt.sign(payload, secret, options) -> string
Creates and signs a new JWT from the given payload.
The payload is merged with any registered claims derived from options before signing. Caller-supplied claims in payload always take precedence over option-derived claims.
Options
| Key | Type | Default | Description |
|---|---|---|---|
| algorithm | string | HS256 | Signing algorithm |
| expires_in | number | : | Token lifetime in seconds |
| not_before | number | : | Seconds until token becomes valid |
| issuer | string | : | Value for the iss claim |
| subject | string | : | Value for the sub claim |
| audience | string|list | : | Value for the aud claim |
| jwt_id | string | auto | Value for the jti claim |
| no_timestamp | bool | false | Omit the iat claim when true |
| header | dict | : | Extra header parameters |
| allow_none | bool | false | Permit the none algorithm |
Example
import jwt
var token = jwt.sign(
{ user_id: 99, role: 'editor' },
'my-secret-key',
{
algorithm: jwt.HS256,
expires_in: 3600,
issuer: 'api.example.com',
subject: '99',
}
)
Parameters
payload(dict)secret(string)options(?dict)
Returns string
Raises AlgorithmError
Raises JwtError
verify()
jwt.verify(token, secret, options) -> dict|Token
Verifies a JWT string and returns its payload (or the full Token when options.complete is true).
Verification performs the following steps in order:
- Structural decode: ensures the token is well-formed. 2. Algorithm check: rejects algorithms not in the options.algorithms list. 3. Signature verification: recomputes and compares the signature using a constant-time comparison for HMAC algorithms, or the underlying public-key verification for PS/ES/EdDSA algorithms. 4. Temporal claim validation: checks exp, nbf, and iat against the current clock, applying clock_tolerance leeway where configured. 5. Registered claim validation: checks iss, aud, and sub when the corresponding options are set.
An error is raised at the first failure; verification does not accumulate errors.
Options
| Key | Type | Default | Description |
|---|---|---|---|
| algorithms | list | [HS256,HS384,HS512] | Accepted algorithm whitelist |
| issuer | string | : | Required iss claim value |
| subject | string | : | Required sub claim value |
| audience | string|list | : | Required aud claim value |
| clock_tolerance | number | 0 | Leeway in seconds for exp, nbf, and iat |
| ignore_expiration | bool | false | Skip exp validation |
| ignore_not_before | bool | false | Skip nbf validation |
| complete | bool | false | Return full Token instead of payload dict |
| allow_none | bool | false | Permit the none algorithm |
Example
import jwt
catch {
var payload = jwt.verify(token, 'my-secret-key', {
algorithms: [jwt.HS256],
issuer: 'api.example.com',
clock_tolerance: 5,
})
echo payload['user_id']
} as e
if e {
# e is a JwtError
echo 'token rejected: ' + e.message
}
Parameters
token(string)secret(string)options(dict)
Returns dict|Token
Raises MalformedTokenError
Raises AlgorithmError
Raises SignatureError
Raises TokenExpiredError
Raises ClaimError
header()
jwt.header(token) -> dict
Returns the decoded header dictionary of a token without verifying its signature.
This is useful for reading the kid or alg header parameters before
selecting the appropriate key or algorithm for full verification.
Parameters
token(string)
Returns dict
Raises MalformedTokenError
payload()
jwt.payload(token) -> dict
Returns the decoded payload dictionary of a token without verifying its signature.
Do not use this to make authorization decisions. Use verify() instead.
Parameters
token(string)
Returns dict
Raises MalformedTokenError
is_expired()
jwt.is_expired(token, leeway) -> bool
Returns true when the token’s exp claim is in the past, without verifying the signature.
A token without an exp claim is never considered expired by this function.
Parameters
token(string)leeway(number) — Optional seconds of tolerance. Default: 0.
Returns bool
Raises MalformedTokenError
expires_in()
jwt.expires_in(token) -> number|nil
Returns the number of seconds until the token expires, based on its exp claim and the current clock. Returns nil when the token has no exp claim. Returns a negative number when the token has already expired.
Parameters
token(string)
Returns number|nil
Raises MalformedTokenError
jwt.errors
import jwt
Everything here is re-exported by
jwt, soimport jwtis enough and the names are called asjwt.*. Importingjwt.errorson its own works too and reaches the same definitions.
The full error hierarchy raised by the jwt module. All of them inherit
from JwtError, so a caller who just wants to catch “something went
wrong with this token” only needs to catch that one class.
Classes
JwtError
class jwt.JwtError < Error
Base error class for all errors raised by the jwt module. Catching this class will intercept any jwt-specific error.
Constructor
jwt.JwtError(message)
Parameters
message(string)
MalformedTokenError
class jwt.MalformedTokenError < JwtError
Raised when a token cannot be decoded because its structure is not valid. This includes tokens that are not three dot-separated segments or that contain malformed base64url or JSON.
Constructor
jwt.MalformedTokenError(message)
SignatureError
class jwt.SignatureError < JwtError
Raised when a token’s signature does not match its header and payload. This indicates the token has been tampered with or was signed with a different key.
Constructor
jwt.SignatureError(message)
TokenExpiredError
class jwt.TokenExpiredError < JwtError
Raised when a token’s temporal claims fail validation:
exp(expiry): token has expired -nbf(not before): token is not yet valid -iat(issued at): token was issued in the future
Constructor
jwt.TokenExpiredError(message)
ClaimError
class jwt.ClaimError < JwtError
Raised when a required claim is absent from the token payload, or when a registered claim (iss, aud, sub) does not match the expected value.
Constructor
jwt.ClaimError(message)
AlgorithmError
class jwt.AlgorithmError < JwtError
Raised when an algorithm specified in a token header is not supported by
this module, when the caller attempts to use the none algorithm without
explicitly enabling it, or when a JWKS document has no key matching a
token’s kid.
Constructor
jwt.AlgorithmError(message)
jwt.jwks
import jwt
Everything here is re-exported by
jwt, soimport jwtis enough and the names are called asjwt.*. Importingjwt.jwkson its own works too and reaches the same definitions.
Jwks, plus verify_with_jwks(): resolving a token’s signing key from
a JSON Web Key Set (RFC 7517) by its kid header, rather than a single
fixed secret/key.
Functions
verify_with_jwks()
jwt.verify_with_jwks(token, jwks: instance, options) -> dict|Token
Verifies a token by resolving its signing key from a JWKS document rather than a single fixed secret.
Reads the token’s kid header parameter, resolves the matching key in
jwks, converts it to a PEM, and verifies the token with it exactly as
verify() would with that PEM passed directly.
Parameters
token(string)jwks(Jwks)options(?dict) — Seeverify()for the full reference; note thatalgorithmsshould still be set explicitly here, the same as with any otherverify()call: resolving a key bykidsays nothing about which algorithm is safe to trust it with.
Returns dict|Token
Raises AlgorithmError When the token has no kid, or no key in
jwks matches it.
Raises MalformedTokenError
Raises SignatureError
Raises TokenExpiredError
Raises ClaimError
Raises CryptoError
Classes
Jwks
class jwt.Jwks
Parses a JSON Web Key Set document (a dict shaped like { keys: [...] },
exactly what json.decode() on a fetched JWKS document produces) and
resolves individual keys by their kid.
Example
import jwt
import json
var jwks = jwt.Jwks(json.decode(fetch_jwks_document()))
var payload = jwt.verify_with_jwks(token, jwks, { issuer: 'auth.example.com' })
Constructor
jwt.Jwks(jwks: dict)
Parameters
jwks(dict) — A decoded JWKS document:{ keys: [...] }. Keys without akidfield are ignored, since there is no way to resolve them by kid.
Jwks.find()
jwt.Jwks.find(kid: string) -> dict
Returns the raw JWK dictionary for the given kid.
Parameters
kid(string)
Returns dict
Raises AlgorithmError When no key with this kid is present.
Jwks.to_pem()
jwt.Jwks.to_pem(kid: string) -> string
Resolves the given kid and converts it to a PEM public key via
crypto.jwk.to_pem().
Parameters
kid(string)
Returns string — PEM-encoded public key.
Raises AlgorithmError
Raises CryptoError
jwt.signer
import jwt
Everything here is re-exported by
jwt, soimport jwtis enough and the names are called asjwt.*. Importingjwt.signeron its own works too and reaches the same definitions.
Signer, a reusable configured wrapper around core.sign().
Classes
Signer
class jwt.Signer
A reusable token signer that holds a fixed secret and set of default signing options.
Using a Signer instance is recommended when the same signing configuration is used throughout an application. Default options can be overridden on individual sign() calls.
Example
import jwt
var signer = jwt.Signer('my-secret', {
algorithm: jwt.HS256,
expires_in: 900,
issuer: 'auth.example.com',
audience: 'api.example.com',
})
var token = signer.sign({ user_id: 7, role: 'admin' })
Constructor
jwt.Signer(secret, options)
Parameters
secret(string) — The secret or PEM private key used for signing.options(dict) — Default signing options (see sign() for reference).
Signer.sign()
jwt.Signer.sign(payload, options) -> string
Signs the given payload using the secret and default options bound to this Signer, returning a token string.
Options passed here are merged with the instance defaults, with per-call options taking precedence.
Parameters
payload(dict)options(dict) — Optional per-call option overrides.
Returns string
Raises AlgorithmError
Raises JwtError
jwt.token
import jwt
Everything here is re-exported by
jwt, soimport jwtis enough and the names are called asjwt.*. Importingjwt.tokenon its own works too and reaches the same definitions.
The Token class returned by decode() and verify() (when
options.complete is set).
Classes
Token
class jwt.Token
Represents a decoded JWT. Instances of this class are returned by decode() and verify(). The raw header and payload dictionaries are available as properties, and individual claims are accessible directly through the index operator.
Example
import jwt
var tok = jwt.decode('ey_j...')
echo tok.header['alg'] # HS256
echo tok.payload['sub'] # '1234567890'
echo tok['sub'] # '1234567890' (shorthand)
Fields
| Field | Type | Description |
|---|---|---|
header | The decoded header dictionary. | |
payload | The decoded payload dictionary. | |
signature | The raw base64url-encoded signature segment as it appeared in the original token string. | |
raw | The original token string from which this instance was decoded. |
Constructor
jwt.Token(header, payload, signature, raw)
Parameters
header(dict)payload(dict)signature(string)raw(string)
Token.set_leeway()
jwt.Token.set_leeway(seconds: number)
Token.get()
jwt.Token.get(name: string) -> any
Returns the value of the named claim from the payload, or nil if the claim is not present.
Parameters
name(string)
Returns any
Token.is_expired()
jwt.Token.is_expired() -> bool
Returns true when the token carries an exp claim and the current time
is past that value, honoring the leeway this Token was verified with
when it came from verify().
A Token produced by a bare decode() (no verification) always has zero
leeway.
Returns bool
jwt.verifier
import jwt
Everything here is re-exported by
jwt, soimport jwtis enough and the names are called asjwt.*. Importingjwt.verifieron its own works too and reaches the same definitions.
Verifier, a reusable configured wrapper around core.verify().
Classes
Verifier
class jwt.Verifier
A reusable token verifier that holds a fixed secret and set of options.
Using a Verifier instance is the recommended approach for applications that verify many tokens with the same configuration, as it avoids passing options on every call and makes the intent of the verification parameters explicit and auditable.
Example
import jwt
import http.middleware
var verifier = jwt.Verifier('my-secret', {
algorithms: [jwt.HS256],
issuer: 'auth.example.com',
audience: 'api.example.com',
clock_tolerance: 10,
})
# In a request handler:
var token = middleware.parse_bearer(req.headers['Authorization'])
var payload = verifier.verify(token)
echo payload['role']
Constructor
jwt.Verifier(secret, options)
Parameters
secret(string) — The secret or PEM public key used for verification.options(dict) — Verification options (see verify() for reference).
Verifier.verify()
jwt.Verifier.verify(token, options) -> dict|Token
Verifies the given token string using the secret and options bound to this Verifier.
Additional options passed here are merged with the instance options, with per-call options taking precedence. This allows overriding individual parameters (such as audience) on a per-request basis without constructing a new Verifier.
Parameters
token(string)options(dict) — Optional per-call option overrides.
Returns dict|Token
Raises MalformedTokenError
Raises AlgorithmError
Raises SignatureError
Raises TokenExpiredError
Raises ClaimError
Verifier.decode()
jwt.Verifier.decode(token) -> Token
Decodes the given token without verification. Delegates to decode().
Parameters
token(string)
Returns Token
Raises MalformedTokenError
uuid
import uuid
Provides RFC 9562 (and RFC 4122) compliant Universally Unique Identifier (UUID) generation, parsing, validation, and inspection.
A UUID is a 128-bit label represented as 32 lowercase hexadecimal digits, displayed in five groups separated by hyphens in the form:
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
where M encodes the version and N encodes the variant.
Supported versions
| Version | Algorithm | RFC |
|---|---|---|
| v1 | Time-based (Gregorian time + MAC) | RFC 4122 |
| v3 | Name-based MD5 | RFC 4122 |
| v4 | Random | RFC 4122 |
| v5 | Name-based SHA-1 | RFC 4122 |
| v6 | Time-ordered (reordered v1) | RFC 9562 |
| v7 | Unix-time ordered + random | RFC 9562 |
| v8 | Custom / application-defined | RFC 9562 |
The Nil UUID (00000000-0000-0000-0000-000000000000) and the Max UUID
(ffffffff-ffff-ffff-ffff-ffffffffffff) are also defined as per RFC
9562.
Note: UUID v2 (DCE Security) is intentionally omitted. RFC 9562 declares v2 “out of scope”, and its specification lives in a separate DCE document rather than in the IETF UUID standard.
Quick start
import uuid
echo uuid.v4() # e.g. '110e8400-e29b-41d4-a716-446655440000'
echo uuid.v7() # time-ordered, database-friendly
echo uuid.is_valid('...')
var id = uuid.UUID('110e8400-e29b-41d4-a716-446655440000')
echo id.version() # 4
echo id.variant() # 'RFC 9562'
echo id.urn() # 'urn:uuid:110e8400-...'
Namespace UUIDs (for v3 / v5)
RFC 9562 §Appendix C pre-defines four namespace UUIDs:
uuid.NAMESPACE_DNS: for fully-qualified domain names -uuid.NAMESPACE_URL: for URLs -uuid.NAMESPACE_OID: for ISO OIDs -uuid.NAMESPACE_X500: for X.500 distinguished names
The uuid API
Every public name in uuid, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
uuid.MAX | constant | The Max UUID: all 128 bits are one. |
uuid.NAMESPACE_DNS | constant | Pre-defined namespace UUID for fully-qualified domain names (FQDN). |
uuid.NAMESPACE_OID | constant | Pre-defined namespace UUID for ISO Object Identifiers (OID). |
uuid.NAMESPACE_URL | constant | Pre-defined namespace UUID for URLs. |
uuid.NAMESPACE_X500 | constant | Pre-defined namespace UUID for X.500 Distinguished Names. |
uuid.NIL | constant | The Nil UUID: all 128 bits are zero. |
uuid.UUID | class | Represents a parsed, immutable UUID value. |
uuid.from_bytes | function | Converts a list of 16 byte integers (0–255) in big-endian order into a canonical UUID string. |
uuid.from_int | function | Converts an integer value (the numeric representation of a 128-bit UUID, as returned by UUID.int()) into a… |
uuid.is_valid | function | Returns true if str is a syntactically valid UUID in canonical form… |
uuid.max_uuid | function | Returns the pre-defined Max UUID string. |
uuid.nil_uuid | function | Returns the pre-defined Nil UUID string. |
uuid.parse | function | Parses a UUID string (in canonical, raw hex, URN, or brace-wrapped form) and returns a UUID object. |
uuid.v1 | function | Generates a UUID Version 1 (time-based) as defined in RFC 4122 §4.1 / RFC 9562 §5.1. |
uuid.v3 | function | Generates a UUID Version 3 (name-based, MD5) as defined in RFC 4122 §4.3 / RFC 9562 §5.3. |
uuid.v4 | function | Generates a UUID Version 4 (random) as defined in RFC 4122 §4.4 / RFC 9562 §5.4. |
uuid.v5 | function | Generates a UUID Version 5 (name-based, SHA-1) as defined in RFC 4122 §4.3 / RFC 9562 §5.5. |
uuid.v6 | function | Generates a UUID Version 6 (time-ordered) as defined in RFC 9562 §5.6. |
uuid.v7 | function | Generates a UUID Version 7 (Unix-time ordered) as defined in RFC 9562 §5.7. |
uuid.v8 | function | Generates a UUID Version 8 (custom / application-defined) as defined in RFC 9562 §5.8. |
uuid.version | function | Returns the version number (1–8) of a UUID string, or nil if the UUID is the Nil or Max special form, or if… |
Constants
NAMESPACE_DNS
uuid.NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'
Pre-defined namespace UUID for fully-qualified domain names (FQDN). Use
with uuid.v3() or uuid.v5() when the name is a DNS hostname.
Value: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
NAMESPACE_URL
uuid.NAMESPACE_URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'
Pre-defined namespace UUID for URLs. Use with uuid.v3() or uuid.v5()
when the name is a URL.
Value: 6ba7b811-9dad-11d1-80b4-00c04fd430c8
NAMESPACE_OID
uuid.NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8'
Pre-defined namespace UUID for ISO Object Identifiers (OID). Use with
uuid.v3() or uuid.v5() when the name is an ISO OID.
Value: 6ba7b812-9dad-11d1-80b4-00c04fd430c8
NAMESPACE_X500
uuid.NAMESPACE_X500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8'
Pre-defined namespace UUID for X.500 Distinguished Names. Use with
uuid.v3() or uuid.v5() when the name is an X.500 DN.
Value: 6ba7b814-9dad-11d1-80b4-00c04fd430c8
NIL
uuid.NIL = '00000000-0000-0000-0000-000000000000'
The Nil UUID: all 128 bits are zero. Defined in RFC 9562 §5.9 as a special UUID that signifies “no value”.
Value: 00000000-0000-0000-0000-000000000000
MAX
uuid.MAX = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
The Max UUID: all 128 bits are one. Defined in RFC 9562 §5.10 as a special UUID often used as a sentinel upper-bound in range queries.
Value: ffffffff-ffff-ffff-ffff-ffffffffffff
Functions
is_valid()
uuid.is_valid(str) -> bool
Returns true if str is a syntactically valid UUID in canonical form
(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), case-insensitive.
This function checks only the structure and character set; it does not verify that the version and variant bits are set to any particular value, so the Nil and Max UUIDs are considered valid.
Example
import uuid
uuid.is_valid('f47ac10b-58cc-4372-a567-0e02b2c3d479') # true
uuid.is_valid('not-a-uuid') # false
uuid.is_valid(uuid.NIL) # true
Parameters
str(string) — The string to test.
Returns bool
version()
uuid.version(str: string) -> number
Returns the version number (1–8) of a UUID string, or nil if the UUID
is the Nil or Max special form, or if str is not a valid UUID.
Parameters
str(string) — A canonical UUID string.
Returns number — | nil
v1()
uuid.v1(node: ?string, clock_seq: ?number) -> string
Generates a UUID Version 1 (time-based) as defined in RFC 4122 §4.1 / RFC 9562 §5.1.
A v1 UUID encodes a 60-bit timestamp counted in 100-nanosecond intervals since the Gregorian epoch (1582-10-15 00:00:00 UTC), a 14-bit clock sequence, and a 48-bit node identifier (MAC address or random).
Privacy note: v1 UUIDs embed timing and node information. For privacy-sensitive contexts prefer
v4()orv7().
Example
import uuid
echo uuid.v1() # e.g. '6ba7b810-9dad-11d1-80b4-00c04fd430c8'
Parameters
|(string) — nil node Optional 12-char hex string for the node field (48-bit MAC address). Ifnil, a random multicast node value is used (RFC 4122 §4.5).|(number) — nil clock_seq Optional 14-bit clock sequence (0–16383). Ifnil, the internal monotonic sequence is used.
Returns string — Canonical UUID string.
v3()
uuid.v3(namespace: string, name: string) -> string
Generates a UUID Version 3 (name-based, MD5) as defined in RFC 4122 §4.3 / RFC 9562 §5.3.
Given the same namespace and name, v3() always returns the same
UUID. The UUID is derived by computing the MD5 hash of the namespace
UUID bytes concatenated with the UTF-8 encoded name bytes, then setting
the version and variant bits.
Note: MD5 is cryptographically broken. For new designs, prefer
v5()(SHA-1) or generate random UUIDs withv4().
Example
import uuid
echo uuid.v3(uuid.NAMESPACE_DNS, 'www.example.com')
# always: '5df41881-3aed-3515-88a7-2f4a814cf09e'
Parameters
namespace(string) — A UUID string to use as the namespace. Use the pre-definedNAMESPACE_*constants or any other valid UUID.name(string) — The name within the namespace.
Returns string — Canonical UUID string.
Raises Error If namespace is not a valid UUID.
v4()
uuid.v4() -> string
Generates a UUID Version 4 (random) as defined in RFC 4122 §4.4 / RFC 9562 §5.4.
122 bits are filled with pseudo-random data; the remaining 6 bits encode
the version (0100) and variant (10).
This is the most commonly used UUID version for general-purpose unique identifiers where time-ordering is not required.
Example
import uuid
echo uuid.v4() # e.g. 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
Returns string — Canonical UUID string.
v5()
uuid.v5(namespace: string, name: string) -> string
Generates a UUID Version 5 (name-based, SHA-1) as defined in RFC 4122 §4.3 / RFC 9562 §5.5.
Identical in structure to v3() but uses SHA-1 instead of MD5. SHA-1 is
preferred over MD5 for new name-based UUIDs. Only the first 128 bits of
the 160-bit SHA-1 digest are used.
Given the same namespace and name, v5() always returns the same
UUID.
Example
import uuid
echo uuid.v5(uuid.NAMESPACE_URL, 'https://www.example.com')
# always: 'a0787afd-c170-5773-8d60-02739168de9f'
Parameters
namespace(string) — A UUID string to use as the namespace. Use the pre-definedNAMESPACE_*constants or any other valid UUID.name(string) — The name within the namespace.
Returns string — Canonical UUID string.
Raises Error If namespace is not a valid UUID.
v6()
uuid.v6(node: ?string, clock_seq: ?number) -> string
Generates a UUID Version 6 (time-ordered) as defined in RFC 9562 §5.6.
v6 is a reordered variant of v1 that places the most significant timestamp bits first, making v6 UUIDs naturally sortable lexicographically by generation time. It retains the same 60-bit Gregorian timestamp, 14-bit clock sequence, and 48-bit node as v1.
v6 is the recommended replacement for v1 when time-ordered, monotonic
IDs derived from a Gregorian clock are required. For most new designs,
v7() (Unix-time based) is simpler and equally sortable.
Example
import uuid
echo uuid.v6() # e.g. '1ef9e292-a7a4-6000-80b4-00c04fd430c8'
Parameters
|(string) — nil node Optional 12-char hex node (seev1()).|(number) — nil clock_seq Optional 14-bit clock sequence (seev1()).
Returns string — Canonical UUID string.
v7()
uuid.v7() -> string
Generates a UUID Version 7 (Unix-time ordered) as defined in RFC 9562 §5.7.
v7 UUIDs embed a 48-bit Unix millisecond timestamp in the most significant bits, followed by a 12-bit sub-millisecond sequence counter (for monotonicity within the same millisecond) and 62 random bits for uniqueness.
v7 is the recommended version for new systems that need:
- Time-ordered, lexicographically sortable identifiers. - Good database index locality (avoids B-tree fragmentation). - No MAC address leakage (unlike v1 / v6).
Example
import uuid
echo uuid.v7() # e.g. '018f5e1a-2b3c-7d4e-9f0a-1b2c3d4e5f6a'
Layout (RFC 9562 §5.7):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Returns string — Canonical UUID string.
v8()
uuid.v8(a: number, b: number, c: number) -> string
Generates a UUID Version 8 (custom / application-defined) as defined in RFC 9562 §5.8.
v8 is intended for vendor-specific or experimental use cases where the caller needs to embed application-defined data into a UUID while retaining the standard format, version, and variant bits. RFC 9562 does not prescribe the meaning of any field beyond the version and variant.
The three parameters together provide 48 + 12 + 62 = 122 bits of application-controlled data.
Example
import uuid
# Embed a shard ID (a), a type tag (b), and a sequence number (c).
echo uuid.v8(0x0123456789ab, 0x0cd, 0x0123456789abcdef)
Parameters
a(number) — 48-bit application-defined field (bits 0–47). Values exceeding 48 bits are silently truncated.b(number) — 12-bit application-defined field (bits 48–59). Values exceeding 12 bits are silently truncated.c(number) — 62-bit application-defined field (bits 64–125). Values exceeding 62 bits are silently truncated.
Returns string — Canonical UUID string.
nil_uuid()
uuid.nil_uuid() -> string
Returns the pre-defined Nil UUID string.
Equivalent to the module-level constant uuid.NIL. Provided as a
function for symmetry with the other generators.
Returns string — '00000000-0000-0000-0000-000000000000'
max_uuid()
uuid.max_uuid() -> string
Returns the pre-defined Max UUID string.
Equivalent to the module-level constant uuid.MAX. Provided as a
function for symmetry with the other generators.
Returns string — 'ffffffff-ffff-ffff-ffff-ffffffffffff'
parse()
uuid.parse(str: string) -> UUID
Parses a UUID string (in canonical, raw hex, URN, or brace-wrapped form)
and returns a UUID object.
This is equivalent to calling UUID(str) directly.
Example
import uuid
var id = uuid.parse('urn:uuid:f47ac10b-58cc-4372-a567-0e02b2c3d479')
echo id.version() # 4
Parameters
str(string) — The UUID string to parse.
Returns UUID
Raises Error If str is not a recognisable UUID form.
from_bytes()
uuid.from_bytes(bytes_list: list|bytes) -> string
Converts a list of 16 byte integers (0–255) in big-endian order into a canonical UUID string.
Parameters
bytes_list(list) — A list of exactly 16 integers in [0, 255].
Returns string — Canonical UUID string.
Raises Error If bytes_list does not contain exactly 16 bytes.
from_int()
uuid.from_int(int_val) -> string
Converts an integer value (the numeric representation of a 128-bit UUID,
as returned by UUID.int()) into a canonical UUID string.
Accepts either a bigint (the exact way to represent a full 128-bit
value, and what UUID.int() itself returns) or a plain number for a
small value that’s within safe integer precision (e.g. from_int(0)).
Parameters
int_val(number|bigint) — The integer value of the UUID.
Returns string — Canonical UUID string.
Classes
UUID
class uuid.UUID
Represents a parsed, immutable UUID value.
Instances expose the canonical string form plus convenience properties and methods for inspecting and converting the UUID.
Example
import uuid
var id = uuid.UUID('f47ac10b-58cc-4372-a567-0e02b2c3d479')
echo id.version() # 4
echo id.variant() # 'RFC 9562'
echo id.urn() # 'urn:uuid:f47ac10b-58cc-4372-a567-0e02b2c3d479'
echo id.hex() # 'f47ac10b58cc4372a5670e02b2c3d479'
echo id.int() # integer value of the 128-bit UUID
Raises Error if the supplied string is not a valid UUID.
Fields
| Field | Type | Description |
|---|---|---|
value | string | The canonical (lowercase, hyphenated) string representation. |
Constructor
uuid.UUID(uuid_str: string)
Creates a new UUID object from a canonical UUID string.
The constructor normalises the input to lowercase and accepts any of the following common forms:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx(canonical, with hyphens) -xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(raw hex, no hyphens) -urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx(URN form) -{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}(brace-wrapped GUID form)
Parameters
uuid_str(string) — The UUID to parse.
Raises Error If uuid_str is not a valid UUID in any accepted
form.
UUID.version()
uuid.UUID.version()
Returns the UUID version number (1–8), or nil for the Nil and Max
special-form UUIDs which carry no version.
The version is encoded in the high nibble of byte 6 (the M position in
the canonical format xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx).
UUID.variant()
uuid.UUID.variant()
Returns a human-readable string describing the UUID variant field.
The variant occupies the high bits of byte 8 (the N position):
| High bits | Variant string |
|---|---|
0xx | 'NCS' |
10x | 'RFC 9562' |
110 | 'Microsoft' |
111 | 'Future' |
UUID.to_string()
uuid.UUID.to_string() -> string
Returns the string representation of the UUID in canonical lowercase
hyphenated form: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
Returns string
UUID.urn()
uuid.UUID.urn() -> string
Returns the UUID as a URN string per RFC 9562 §4:
urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Returns string
UUID.hex()
uuid.UUID.hex() -> string
Returns the raw 32-character lowercase hex representation of the UUID with all hyphens removed.
Returns string
UUID.int()
uuid.UUID.int() -> bigint
Returns the UUID as an integer (the numeric value of its 128 bits), as
an exact bigint: a 128-bit value is always far beyond what a regular
number can represent exactly, so this never uses one. Pass the result
straight to from_int() to convert back.
Returns bigint
UUID.bytes()
uuid.UUID.bytes() -> list
Returns the UUID as a 16-element list of byte values (integers 0–255), in big-endian (network) byte order.
Returns list
UUID.is_nil()
uuid.UUID.is_nil() -> bool
Returns true if this UUID is the Nil UUID
(00000000-0000-0000-0000-000000000000).
Returns bool
UUID.is_max()
uuid.UUID.is_max() -> bool
Returns true if this UUID is the Max UUID
(ffffffff-ffff-ffff-ffff-ffffffffffff).
Returns bool
UUID.equals()
uuid.UUID.equals(other) -> bool
Compares this UUID with another UUID or canonical UUID string. Returns
true if both UUIDs represent the same 128-bit value.
Parameters
|(UUID) — string other The UUID to compare against.
Returns bool
Raises TypeError if other is neither a UUID nor a string.
UUID.compare()
uuid.UUID.compare(other: instance) -> number
Compares this UUID with another UUID lexicographically by canonical string form. For time-ordered versions (v1, v6, v7) this is also a comparison by generation time.
Parameters
other(UUID)
Returns number — A negative number if this UUID sorts before
other, a positive number if after, 0 if they’re equal.
2026, Richard Ore and The Zuri Contributors
validate
import validate
Schema-based input validation for Zuri applications.
The validate module provides a fluent, chainable API for defining
field-level validation rules, composing them into schemas, and checking
arbitrary data dictionaries against those schemas.
Quick start
import validate
var schema = validate.schema({
name: validate.required().string().max_length(100),
email: validate.required().string().email(),
age: validate.required().integer().between(18, 120),
website: validate.string().url().sometimes(),
})
var result = schema.check({
name: 'Ada Lovelace',
email: 'ada@example.com',
age: 36,
})
if result.valid {
echo 'All good.'
} else {
for err in result.errors {
echo '${err.field}: ${err.message}'
}
}
Rule chaining
Every top-level function returns a Validator instance. Rules are
evaluated in the order they are chained. By default, evaluation stops at
the first failing rule for each field (bail mode). Pass .bail(false)
to collect all errors for a field.
# Collects every error for "password" rather than stopping at the first.
var rules = validate.schema({
password: validate.required().string()
.min_length(8)
.regex('/[A-Z]/')
.regex('/[0-9]/')
.bail(false),
})
Optional fields
var rules = validate.schema({
# bio is optional; when present it must be at most 500 chars.
bio: validate.string().max_length(500).sometimes(),
# nickname is optional and may be nil.
nickname: validate.nullable().string().max_length(30).sometimes(),
})
Cross-field validation
var schema = validate.schema({
password: validate.required().string().min_length(8),
password_confirmation: validate.required().string().confirmed(),
role: validate.required().string().is_in(['user', 'admin']),
admin_code: validate.required_if('role', 'admin').string(),
})
required_if() (like the other conditional-requirement functions below)
must come before .string() in the chain: see “Sequential Validation”
further down for why the order here matters.
Nested fields (dot-notation)
var schema = validate.schema({
'address.street': validate.required().string(),
'address.city': validate.required().string(),
'address.country': validate.required().string().length(2).uppercase(),
})
List items (wildcard)
var schema = validate.schema({
'tags.*': validate.required().string().max_length(32).alpha_dash(),
})
Custom rules
Inline function:
var schema = validate.schema({
code: validate.required().string().custom(@(value) {
return value.starts_with('ZURI-') and value.length() == 12
}, 'Must be a valid Zuri code (ZURI-XXXXXXX).')
})
Subclassing Rule:
import validate { Rule, Validator }
class Slug < Rule {
validate(value) {
if !is_string(value) return false
return !!value.match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/')
}
error() { return 'The ${self.name} field must be a valid slug.' }
}
var schema = validate.schema({
slug: Validator().required().use(Slug),
})
Raising on failure
import validate { ValidationError }
def create_user(req, res) {
catch {
schema.check_or_raise(req.body)
} as e
if e {
return res.json({ errors: schema.group_errors(e.errors) }, 422)
}
return res.json({ ok: true })
}
HTTP handler pattern
import http
import validate
import validate { ValidationError }
var create_user_schema = validate.schema({
name: validate.required().string().max_length(100),
email: validate.required().string().email(),
password: validate.required().string().min_length(8),
role: validate.required().string().is_in(['user', 'admin']).sometimes(),
})
server.handle('POST', '/users', @(req, res) {
catch {
create_user_schema.check_or_raise(req.body)
} as e
if e {
return res.json({ errors: create_user_schema.group_errors(e.errors) }, 422)
}
# ... create user
res.json({ status: 'created' }, 201)
})
Sequential Validation
The validation mode is sequential, meaning that each field is validated in turn. This is the behavior you expect from a form submission. It is important to keep this in mind when using this module.
Consider the example below:
var schema = validate.schema({
name: validate.required_if('age', 30).string().max_length(100),
age: validate.required().integer().between(18, 120),
})
If the age field is empty, the validation of name will not raise any
error. This is because the required_if rule is only applied if the age
field is 30 or greater and the entire chain exists before the next rule
string().
However, if the validation rules were modified like this:
var schema = validate.schema({
name: validate.string().required_if('age', 30).max_length(100),
age: validate.required().integer().between(18, 120),
})
Even when the age field is empty, the validation of name will raise
an error if it is empty. This is because the string rule comes before
the required_if rule.
With this behavior in mind, it is advised that optional rules such as
sometimes, nullable, *_if, *_unless rules are applied early in
the chain unless you absolutely want them to fail irrespective.
The validate API
Every public name in validate, wherever it is declared. Each links to
the page that documents it.
| Name | Kind | Summary |
|---|---|---|
validate.Rule | class | Abstract base class for all validation rules. |
validate.Schema | class | Binds a ruleset to data and validates it. |
validate.ValidationError | class | Raised by Schema.check_or_raise() when validation fails. |
validate.Validator | class | Fluent rule chain builder for a single field. |
validate.accepted | function | The value must be one of the accepted truthy representations: true, "true", "yes", "on", "1", 1. |
validate.after | function | The value must be a date string after ref. |
validate.after_or_equal | function | The value must be a date string after or equal to ref. |
validate.alpha | function | The string must contain only ASCII alphabetic characters (a-z, A-Z). |
validate.alpha_dash | function | The string must contain only ASCII alphanumeric characters, hyphens, and underscores. |
validate.alpha_num | function | The string must contain only ASCII alphanumeric characters. |
validate.before | function | The value must be a date string before ref. |
validate.before_or_equal | function | The value must be a date string before or equal to ref. |
validate.between | function | The value’s size must be between min and max (inclusive). |
validate.boolean | function | The value must be boolean-like: true, false, "true", "false", "1", "0", 1, or 0. |
validate.confirmed | function | The value must match the ``field_name_confirmation sibling field. |
validate.contains | function | The string must contain the given substring. |
validate.custom | function | Validates with an inline anonymous function. |
validate.custom_with_data | function | Validates with an inline function that also receives the full data dictionary, enabling cross-field logic… |
validate.date | function | The value must be a valid date string parseable by the date module. |
validate.declined | function | The value must be one of the declined falsy representations: false, "false", "no", "off", "0", 0. |
validate.dict | function | The value must be a dictionary. |
validate.different | function | The value must differ from the value of other_field. |
validate.distinct | function | The list must not contain duplicate values. |
validate.doesnt_contain | function | The string must not contain the given substring. |
validate.doesnt_end_with | function | The string must not end with any of the given suffixes. |
validate.doesnt_start_with | function | The string must not start with any of the given prefixes. |
validate.each | function | Every item in the list must pass the given Validator chain. |
validate.email | function | The value must be a syntactically valid email address. |
validate.ends_with | function | The string must end with one of the given suffixes. |
validate.equals | function | The value must strictly equal expected. |
validate.gt | function | The numeric value must be greater than n. |
validate.gte | function | The numeric value must be greater than or equal to n. |
validate.in_list | function | Every item in the list value must be contained in values. |
validate.integer | function | The value must be an integer (no fractional part). |
validate.ip | function | The value must be a valid IPv4 or IPv6 address. |
validate.ipv4 | function | The value must be a valid IPv4 address. |
validate.ipv6 | function | The value must be a valid IPv6 address. |
validate.is_in | function | The value must be one of the given allowed values (strict comparison). |
validate.is_nil | function | The value must be nil. |
validate.json | function | The value must be a valid JSON string. |
validate.length | function | The string’s character count must equal exactly n. |
validate.length_between | function | The string’s length must be between min and max characters (inclusive). |
validate.list | function | The value must be a list. |
validate.lowercase | function | The string must be entirely lowercase. |
validate.lt | function | The numeric value must be less than n. |
validate.lte | function | The numeric value must be less than or equal to n. |
validate.max | function | The value’s size must be at most max. |
validate.max_items | function | The list must have at most max items. |
validate.max_length | function | The string must be at most max characters long. |
validate.min | function | The value’s size must be at least min. |
validate.min_items | function | The list must have at least min items. |
validate.min_length | function | The string must be at least min characters long. |
validate.multiple_of | function | The numeric value must be a multiple of n. |
validate.negative | function | The numeric value must be strictly negative (less than zero). |
validate.negative_or_zero | function | The numeric value must be zero or negative. |
validate.not_blank | function | The string must not consist entirely of whitespace. |
validate.not_equals | function | The value must not equal forbidden. |
validate.not_in | function | The value must not be one of the given forbidden values. |
validate.not_nil | function | The value must not be nil. |
validate.not_regex | function | The string must not match the given regular expression pattern. |
validate.nullable | function | A nil value passes all subsequent rules without evaluation. |
validate.number | function | The value must be a number (integer or float). |
validate.numeric | function | The value must be numeric: either a number type or a string that converts cleanly to a number. |
validate.positive | function | The numeric value must be strictly positive (greater than zero). |
validate.positive_or_zero | function | The numeric value must be zero or positive. |
validate.prohibited | function | This field must be absent or nil: it is never permitted. |
validate.prohibited_if | function | The field must be absent when other_field equals any of other_values. |
validate.prohibited_unless | function | The field must be absent unless other_field equals any of other_values. |
validate.prohibits | function | When this field is present, none of the listed sibling fields may also be present. |
validate.regex | function | The string must match the given regular expression pattern. |
validate.required | function | The field must be present, non-nil, and non-empty. |
validate.required_if | function | The field becomes required when other_field equals any value in other_values. |
validate.required_unless | function | The field becomes required unless other_field equals any value in other_values. |
validate.required_with | function | The field becomes required when any of the listed sibling fields are present and non-blank. |
validate.required_with_all | function | The field becomes required when all of the listed sibling fields are present and non-blank. |
validate.required_without | function | The field becomes required when any of the listed sibling fields are absent or blank. |
validate.required_without_all | function | The field becomes required when all of the listed sibling fields are absent or blank. |
validate.rule.instantiate_rule | function | Builds one Rule instance from a [rule_class, ...args] entry: the shape Validator._rules stores each… |
validate.rules.Accepted | class | Passes when the value equals one of the specified accepted values: true, "true", "yes", "on", "1",… |
validate.rules.After | class | Passes when the value is a date string after the given reference date. |
validate.rules.AfterOrEqual | class | Passes when the value is a date string after or equal to the given reference date. |
validate.rules.Alpha | class | Passes when the string contains only ASCII alphabetic characters (a-z, A-Z). |
validate.rules.AlphaDash | class | Passes when the string contains only ASCII alphanumeric characters, hyphens (-), and underscores (_). |
validate.rules.AlphaNum | class | Passes when the string contains only ASCII alphanumeric characters. |
validate.rules.Before | class | Passes when the value is a date string before the given reference date. |
validate.rules.BeforeOrEqual | class | Passes when the value is a date string before or equal to the given reference date. |
validate.rules.Between | class | Passes when the value’s size is between min and max (inclusive). |
validate.rules.BooleanRule | class | Passes only when the value is a boolean (true or false). |
validate.rules.Confirmed | class | Passes when the field’s value matches ``name_confirmation in the data. |
validate.rules.Contains | class | Passes when the string contains the given substring. |
validate.rules.CustomRule | class | Custom rule backed by a caller-supplied function. |
validate.rules.CustomRuleWithData | class | Custom rule that also receives the full data dictionary. |
validate.rules.DateRule | class | Passes when the value is a valid date string parseable by the date module (e.g. "2024-06-15",… |
validate.rules.Declined | class | Passes when the value equals one of the specified declined values: false, "false", "no", "off",… |
validate.rules.DictRule | class | Passes only when the value is a dictionary. |
validate.rules.Different | class | Passes when the field’s value does not match the value of another field. |
validate.rules.Distinct | class | Passes when the list field contains no duplicate values. |
validate.rules.DoesntContain | class | Passes when the string does not contain the given substring. |
validate.rules.DoesntEndWith | class | Passes when the string does not end with any of the given suffixes. |
validate.rules.DoesntStartWith | class | Passes when the string does not start with any of the given prefixes. |
validate.rules.Each | class | Passes when every item in the list satisfies the given Validator chain. |
validate.rules.Email | class | Passes when the value is a syntactically valid email address. |
validate.rules.EndsWith | class | Passes when the string ends with one of the given suffixes. |
validate.rules.Equals | class | Passes when the field value is strictly equal to the given expected value. |
validate.rules.Gt | class | Passes when the numeric value is greater than n. |
validate.rules.Gte | class | Passes when the numeric value is greater than or equal to n. |
validate.rules.In | class | Passes when the value is contained in the given list of allowed values. |
validate.rules.InList | class | Passes when every item in the value list is contained in the allowed list. |
validate.rules.IntegerRule | class | Passes only when the value is an integer (no fractional part). |
validate.rules.Ip | class | Passes when the value is a valid IPv4 or IPv6 address. |
validate.rules.Ipv4 | class | Passes when the value is a valid IPv4 address. |
validate.rules.Ipv6 | class | Passes when the value is a valid IPv6 address. |
validate.rules.Json | class | Passes when the value is a valid JSON string. |
validate.rules.Length | class | Passes when a string’s length is exactly n characters. |
validate.rules.LengthBetween | class | Passes when a string’s length is between min and max characters (inclusive). |
validate.rules.ListRule | class | Passes only when the value is a list. |
validate.rules.Lowercase | class | Passes when the string value contains only lowercase characters. |
validate.rules.Lt | class | Passes when the numeric value is less than n. |
validate.rules.Lte | class | Passes when the numeric value is less than or equal to n. |
validate.rules.Max | class | Passes when the value’s size is at most max. |
validate.rules.MaxItems | class | Passes when the list field has at most max items. |
validate.rules.MaxLength | class | Passes when a string’s length is at most max characters. |
validate.rules.Min | class | Passes when the value’s size is at least min. |
validate.rules.MinItems | class | Passes when the list field has at least min items. |
validate.rules.MinLength | class | Passes when a string’s length is at least min characters. |
validate.rules.MultipleOf | class | Passes when the numeric value is a multiple of n. |
validate.rules.Negative | class | Passes when the numeric value is negative (strictly less than zero). |
validate.rules.NegativeOrZero | class | Passes when the numeric value is negative or zero. |
validate.rules.Nil | class | Passes when the value is nil. |
validate.rules.NotBlank | class | Passes when the value does not consist solely of whitespace. |
validate.rules.NotEquals | class | Passes when the field value is not equal to the given forbidden value. |
validate.rules.NotIn | class | Passes when the value is not contained in the given list of values. |
validate.rules.NotNil | class | Passes when the value is not nil. |
validate.rules.NotRegex | class | Passes when the string value does not match the given regular expression. |
validate.rules.Nullable | class | Passes when the field is absent or nil. |
validate.rules.NumberRule | class | Passes only when the value is a number (integer or float). |
validate.rules.NumericRule | class | Passes only when the value is numeric (a number, or a string that can be losslessly converted to a number). |
validate.rules.Positive | class | Passes when the numeric value is positive (strictly greater than zero). |
validate.rules.PositiveOrZero | class | Passes when the numeric value is positive or zero. |
validate.rules.Prohibited | class | Passes when this field is absent or nil. |
validate.rules.ProhibitedIf | class | Passes when this field is absent or nil if another field equals one of the given values. |
validate.rules.ProhibitedUnless | class | Passes when this field is absent or nil unless another field equals one of the given values. |
validate.rules.Prohibits | class | Passes when both this field and the listed sibling fields are either all present (non-nil) or all absent (nil… |
validate.rules.Regex | class | Passes when the string value matches the given regular expression. |
validate.rules.Required | class | Fails when the field is absent, nil, an empty string, or an empty list. |
validate.rules.RequiredIf | class | Passes when this field is present and non-nil only if another field in the data satisfies a given condition. |
validate.rules.RequiredUnless | class | Passes when this field is present and non-nil unless another field equals any of the given values. |
validate.rules.RequiredWith | class | Passes when this field is present and non-nil if any of the listed sibling fields are also present and… |
validate.rules.RequiredWithAll | class | Passes when this field is present and non-nil if all of the listed sibling fields are present and non-nil. |
validate.rules.RequiredWithout | class | Passes when this field is present and non-nil if any of the listed sibling fields are absent or nil. |
validate.rules.RequiredWithoutAll | class | Passes when this field is present and non-nil if all of the listed sibling fields are absent or nil. |
validate.rules.Same | class | Passes when the field’s value matches the value of another field in the same data dictionary. |
validate.rules.Size | class | Passes when the value’s size equals n. |
validate.rules.StartsWith | class | Passes when the string starts with one of the given prefixes. |
validate.rules.StringRule | class | Passes only when the value is a string. |
validate.rules.Timezone | class | Passes when the value is a real IANA timezone identifier (e.g. "UTC", "Africa/Lagos",… |
validate.rules.Uppercase | class | Passes when the string value contains only uppercase characters. |
validate.rules.Url | class | Passes when the value is a syntactically valid HTTP or HTTPS URL. |
validate.rules.Uuid | class | Passes when the value is a valid canonical UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). |
validate.same | function | The value must match the value of other_field in the same data dictionary. |
validate.schema | function | Creates a new Schema from the given ruleset dictionary. |
validate.size | function | The value’s size must equal exactly n. |
validate.sometimes | function | Skips the entire rule chain when the field is absent from the data dictionary or its value is nil / blank. |
validate.starts_with | function | The string must start with one of the given prefixes. |
validate.string | function | The value must be a string. |
validate.timezone | function | The value must be a valid timezone identifier recognised by the date module (e.g. "UTC",… |
validate.uppercase | function | The string must be entirely uppercase. |
validate.url | function | The value must be a valid HTTP or HTTPS URL. |
validate.use | function | Attaches a pre-defined Rule subclass (the class itself, not an instance) directly. |
validate.uuid | function | The value must be a valid canonical UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). |
validate.value | function | Returns a bare Validator with no rules pre-applied. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
validate.rule | validate.rule.* | Defines the base Rule class that all built-in and custom validation rules extend. |
validate.rules | import validate.rules | All built-in validation rule implementations. |
validate.schema | validate.schema.* | |
validate.validator | validate.validator.* | |
validate.validators | validate.* | Top-level convenience functions that create a fresh Validator instance with one rule pre-applied. |
Functions
schema()
validate.schema(ruleset) -> Schema
Creates a new Schema from the given ruleset dictionary.
This is the primary entry point for defining a validation schema. Each
key is a field name (supports dot-notation and .* wildcards) and each
value must be a Validator instance.
import validate
var schema = validate.schema({
username: validate.required().string().alpha_dash().max_length(30),
email: validate.required().string().email(),
})
Parameters
ruleset(dict)
Returns Schema
Raises ArgumentError
Raises ValueError
2026, Richard Ore and The Zuri Contributors
validate.rule
import validate.rule
validatelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledvalidate.rule.*needsimport validate.rule.
Defines the base Rule class that all built-in and custom validation
rules extend. To create a custom rule, subclass Rule and implement
validate(value) and error().
Custom rule example
import validate { Rule, Validator }
class Palindrome < Rule {
validate(value) {
if !is_string(value) return false
var s = value.lower()
return s == s.reverse()
}
error() {
return '${self.name} must be a palindrome'
}
}
# Use directly on a Validator:
var v = Validator().use(Palindrome)
Functions
instantiate_rule()
validate.rule.instantiate_rule(entry, field_name) -> Rule
Builds one Rule instance from a [rule_class, ...args] entry: the
shape Validator._rules stores each chained rule as: with field_name
prepended as the rule’s first constructor argument.
Shared by Schema (running a field’s whole chain) and Each (running a
nested chain once per list item), which is why this lives here rather
than as a private helper duplicated in both: schema.zu and rules.zu
both already depend on this file, and neither depends on the other, so
this is the one shared place both can reach without a circular import.
Not re-exported from index.zu: this is internal machinery for the
module itself, not part of its public API.
Parameters
entry(list) —[rule_class, ...constructor_args].field_name(string)
Returns Rule
Raises Error When entry has more than 3 extra constructor
arguments.
Classes
Rule
class validate.Rule
Abstract base class for all validation rules.
Subclass this to create custom rules. The name property is injected by
the Schema before validation runs, so error messages can reference the
field name without the rule needing to know it at construction time.
Only validate and error need to be overridden. Rules that require
constructor parameters should define their own constructor and call
parent(name): or accept parameters before name and forward it.
The _data property is injected by the Schema before each field is
validated when needs_data returns true. Rules that must compare
against sibling fields (e.g. same, different, confirmed) should
override needs_data() to return true and read self._data inside
validate.
Fields
| Field | Type | Description |
|---|---|---|
name | The name of the field being validated. |
Constructor
validate.Rule(name)
Parameters
name(string) — Field name: injected automatically by Schema.
Rule.set_data()
validate.Rule.set_data(data)
Sets the full data dictionary this rule can read from validate().
Called by Schema right after construction, only for rules whose
needs_data() returns true: a real public setter so Schema never
needs private-field access into a Rule it doesn’t own.
Parameters
data(dict)
Rule.validate()
validate.Rule.validate(value) -> bool
Returns true when the value satisfies this rule, false otherwise.
Parameters
value(any) — The value of the field being validated.
Returns bool
Rule.error()
validate.Rule.error() -> string
Returns the human-readable error message for this rule when validation
fails. The message should reference self.name for the field label.
Returns string
Rule.needs_data()
validate.Rule.needs_data() -> bool
Override and return true when this rule needs access to the full data
dictionary (e.g. to compare against another field). When true, the
Schema injects the data dictionary into self._data before calling
validate.
Returns bool
Rule.bails_all()
validate.Rule.bails_all() -> bool
Override and return true when this rule should bail out of all
validation chains whether it fails or not, instead of continuing to the
next rule. When true, the Schema will stop validating the field but
will not raise a ValidationError.
Returns bool
validate.rules
import validate.rules
validatedoes not re-export this module, so it is reached only by importing it directly.
All built-in validation rule implementations. Rules are plain classes
that extend Rule. They are not intended to be constructed directly;
the Validator fluent builder and Schema handle construction and
injection automatically.
Classes
Required
class validate.rules.Required < Rule
Fails when the field is absent, nil, an empty string, or an empty list.
Required.validate()
validate.rules.Required.validate(value)
Required.error()
validate.rules.Required.error()
Nullable
class validate.rules.Nullable < Rule
Passes when the field is absent or nil. Use this to mark a field as
optional while still attaching other rules that run only when a value is
present. Pair with sometimes on the Validator.
Nullable.validate()
validate.rules.Nullable.validate(value)
Nullable.error()
validate.rules.Nullable.error()
StringRule
class validate.rules.StringRule < Rule
Passes only when the value is a string.
StringRule.validate()
validate.rules.StringRule.validate(value)
StringRule.error()
validate.rules.StringRule.error()
NumberRule
class validate.rules.NumberRule < Rule
Passes only when the value is a number (integer or float).
NumberRule.validate()
validate.rules.NumberRule.validate(value)
NumberRule.error()
validate.rules.NumberRule.error()
IntegerRule
class validate.rules.IntegerRule < Rule
Passes only when the value is an integer (no fractional part).
IntegerRule.validate()
validate.rules.IntegerRule.validate(value)
IntegerRule.error()
validate.rules.IntegerRule.error()
BooleanRule
class validate.rules.BooleanRule < Rule
Passes only when the value is a boolean (true or false). Also
accepts the strings "true", "false", "1", "0", and the integers
1 and 0.
BooleanRule.validate()
validate.rules.BooleanRule.validate(value)
BooleanRule.error()
validate.rules.BooleanRule.error()
ListRule
class validate.rules.ListRule < Rule
Passes only when the value is a list.
ListRule.validate()
validate.rules.ListRule.validate(value)
ListRule.error()
validate.rules.ListRule.error()
DictRule
class validate.rules.DictRule < Rule
Passes only when the value is a dictionary.
DictRule.validate()
validate.rules.DictRule.validate(value)
DictRule.error()
validate.rules.DictRule.error()
NumericRule
class validate.rules.NumericRule < Rule
Passes only when the value is numeric (a number, or a string that can be losslessly converted to a number).
NumericRule.validate()
validate.rules.NumericRule.validate(value)
NumericRule.error()
validate.rules.NumericRule.error()
Size
class validate.rules.Size < Rule
Passes when the value’s size equals n.
Size is measured as: character count for strings, item count for lists and dicts, and numeric value for numbers.
Constructor
validate.rules.Size(name, n)
Size.validate()
validate.rules.Size.validate(value)
Size.error()
validate.rules.Size.error()
Min
class validate.rules.Min < Rule
Passes when the value’s size is at least min.
Constructor
validate.rules.Min(name, min)
Min.validate()
validate.rules.Min.validate(value)
Min.error()
validate.rules.Min.error()
Max
class validate.rules.Max < Rule
Passes when the value’s size is at most max.
Constructor
validate.rules.Max(name, max)
Max.validate()
validate.rules.Max.validate(value)
Max.error()
validate.rules.Max.error()
Between
class validate.rules.Between < Rule
Passes when the value’s size is between min and max (inclusive).
Constructor
validate.rules.Between(name, min, max)
Between.validate()
validate.rules.Between.validate(value)
Between.error()
validate.rules.Between.error()
Length
class validate.rules.Length < Rule
Passes when a string’s length is exactly n characters.
Constructor
validate.rules.Length(name, n)
Length.validate()
validate.rules.Length.validate(value)
Length.error()
validate.rules.Length.error()
MinLength
class validate.rules.MinLength < Rule
Passes when a string’s length is at least min characters.
Constructor
validate.rules.MinLength(name, min)
MinLength.validate()
validate.rules.MinLength.validate(value)
MinLength.error()
validate.rules.MinLength.error()
MaxLength
class validate.rules.MaxLength < Rule
Passes when a string’s length is at most max characters.
Constructor
validate.rules.MaxLength(name, max)
MaxLength.validate()
validate.rules.MaxLength.validate(value)
MaxLength.error()
validate.rules.MaxLength.error()
LengthBetween
class validate.rules.LengthBetween < Rule
Passes when a string’s length is between min and max characters
(inclusive).
Constructor
validate.rules.LengthBetween(name, min, max)
LengthBetween.validate()
validate.rules.LengthBetween.validate(value)
LengthBetween.error()
validate.rules.LengthBetween.error()
Gt
class validate.rules.Gt < Rule
Passes when the numeric value is greater than n.
Constructor
validate.rules.Gt(name, n)
Gt.validate()
validate.rules.Gt.validate(value)
Gt.error()
validate.rules.Gt.error()
Gte
class validate.rules.Gte < Rule
Passes when the numeric value is greater than or equal to n.
Constructor
validate.rules.Gte(name, n)
Gte.validate()
validate.rules.Gte.validate(value)
Gte.error()
validate.rules.Gte.error()
Lt
class validate.rules.Lt < Rule
Passes when the numeric value is less than n.
Constructor
validate.rules.Lt(name, n)
Lt.validate()
validate.rules.Lt.validate(value)
Lt.error()
validate.rules.Lt.error()
Lte
class validate.rules.Lte < Rule
Passes when the numeric value is less than or equal to n.
Constructor
validate.rules.Lte(name, n)
Lte.validate()
validate.rules.Lte.validate(value)
Lte.error()
validate.rules.Lte.error()
Positive
class validate.rules.Positive < Rule
Passes when the numeric value is positive (strictly greater than zero).
Positive.validate()
validate.rules.Positive.validate(value)
Positive.error()
validate.rules.Positive.error()
Negative
class validate.rules.Negative < Rule
Passes when the numeric value is negative (strictly less than zero).
Negative.validate()
validate.rules.Negative.validate(value)
Negative.error()
validate.rules.Negative.error()
PositiveOrZero
class validate.rules.PositiveOrZero < Rule
Passes when the numeric value is positive or zero.
PositiveOrZero.validate()
validate.rules.PositiveOrZero.validate(value)
PositiveOrZero.error()
validate.rules.PositiveOrZero.error()
NegativeOrZero
class validate.rules.NegativeOrZero < Rule
Passes when the numeric value is negative or zero.
NegativeOrZero.validate()
validate.rules.NegativeOrZero.validate(value)
NegativeOrZero.error()
validate.rules.NegativeOrZero.error()
MultipleOf
class validate.rules.MultipleOf < Rule
Passes when the numeric value is a multiple of n.
Constructor
validate.rules.MultipleOf(name, n)
MultipleOf.validate()
validate.rules.MultipleOf.validate(value)
MultipleOf.error()
validate.rules.MultipleOf.error()
Alpha
class validate.rules.Alpha < Rule
Passes when the string contains only ASCII alphabetic characters (a-z, A-Z).
Alpha.validate()
validate.rules.Alpha.validate(value)
Alpha.error()
validate.rules.Alpha.error()
AlphaNum
class validate.rules.AlphaNum < Rule
Passes when the string contains only ASCII alphanumeric characters.
AlphaNum.validate()
validate.rules.AlphaNum.validate(value)
AlphaNum.error()
validate.rules.AlphaNum.error()
AlphaDash
class validate.rules.AlphaDash < Rule
Passes when the string contains only ASCII alphanumeric characters,
hyphens (-), and underscores (_).
AlphaDash.validate()
validate.rules.AlphaDash.validate(value)
AlphaDash.error()
validate.rules.AlphaDash.error()
StartsWith
class validate.rules.StartsWith < Rule
Passes when the string starts with one of the given prefixes.
Constructor
validate.rules.StartsWith(name, prefixes)
StartsWith.validate()
validate.rules.StartsWith.validate(value)
StartsWith.error()
validate.rules.StartsWith.error()
DoesntStartWith
class validate.rules.DoesntStartWith < Rule
Passes when the string does not start with any of the given prefixes.
Constructor
validate.rules.DoesntStartWith(name, prefixes)
DoesntStartWith.validate()
validate.rules.DoesntStartWith.validate(value)
DoesntStartWith.error()
validate.rules.DoesntStartWith.error()
EndsWith
class validate.rules.EndsWith < Rule
Passes when the string ends with one of the given suffixes.
Constructor
validate.rules.EndsWith(name, suffixes)
EndsWith.validate()
validate.rules.EndsWith.validate(value)
EndsWith.error()
validate.rules.EndsWith.error()
DoesntEndWith
class validate.rules.DoesntEndWith < Rule
Passes when the string does not end with any of the given suffixes.
Constructor
validate.rules.DoesntEndWith(name, suffixes)
DoesntEndWith.validate()
validate.rules.DoesntEndWith.validate(value)
DoesntEndWith.error()
validate.rules.DoesntEndWith.error()
Contains
class validate.rules.Contains < Rule
Passes when the string contains the given substring.
Constructor
validate.rules.Contains(name, needle)
Contains.validate()
validate.rules.Contains.validate(value)
Contains.error()
validate.rules.Contains.error()
DoesntContain
class validate.rules.DoesntContain < Rule
Passes when the string does not contain the given substring.
Constructor
validate.rules.DoesntContain(name, needle)
DoesntContain.validate()
validate.rules.DoesntContain.validate(value)
DoesntContain.error()
validate.rules.DoesntContain.error()
Regex
class validate.rules.Regex < Rule
Passes when the string value matches the given regular expression.
Constructor
validate.rules.Regex(name, pattern)
Regex.validate()
validate.rules.Regex.validate(value)
Regex.error()
validate.rules.Regex.error()
NotRegex
class validate.rules.NotRegex < Rule
Passes when the string value does not match the given regular expression.
Constructor
validate.rules.NotRegex(name, pattern)
NotRegex.validate()
validate.rules.NotRegex.validate(value)
NotRegex.error()
validate.rules.NotRegex.error()
Lowercase
class validate.rules.Lowercase < Rule
Passes when the string value contains only lowercase characters.
Lowercase.validate()
validate.rules.Lowercase.validate(value)
Lowercase.error()
validate.rules.Lowercase.error()
Uppercase
class validate.rules.Uppercase < Rule
Passes when the string value contains only uppercase characters.
Uppercase.validate()
validate.rules.Uppercase.validate(value)
Uppercase.error()
validate.rules.Uppercase.error()
NotBlank
class validate.rules.NotBlank < Rule
Passes when the value does not consist solely of whitespace. Use on
string fields to prevent inputs like " ".
NotBlank.validate()
validate.rules.NotBlank.validate(value)
NotBlank.error()
validate.rules.NotBlank.error()
Email
class validate.rules.Email < Rule
Passes when the value is a syntactically valid email address.
Validates the structure local@domain.tld per a practical subset of RFC
5321. Does not perform DNS MX lookup.
Email.validate()
validate.rules.Email.validate(value)
Email.error()
validate.rules.Email.error()
Url
class validate.rules.Url < Rule
Passes when the value is a syntactically valid HTTP or HTTPS URL.
Url.validate()
validate.rules.Url.validate(value)
Url.error()
validate.rules.Url.error()
Ipv4
class validate.rules.Ipv4 < Rule
Passes when the value is a valid IPv4 address.
Ipv4.validate()
validate.rules.Ipv4.validate(value)
Ipv4.error()
validate.rules.Ipv4.error()
Ipv6
class validate.rules.Ipv6 < Rule
Passes when the value is a valid IPv6 address.
Ipv6.validate()
validate.rules.Ipv6.validate(value)
Ipv6.error()
validate.rules.Ipv6.error()
Ip
class validate.rules.Ip < Rule
Passes when the value is a valid IPv4 or IPv6 address.
Ip.validate()
validate.rules.Ip.validate(value)
Ip.error()
validate.rules.Ip.error()
Uuid
class validate.rules.Uuid < Rule
Passes when the value is a valid canonical UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
Uuid.validate()
validate.rules.Uuid.validate(value)
Uuid.error()
validate.rules.Uuid.error()
Json
class validate.rules.Json < Rule
Passes when the value is a valid JSON string.
Json.validate()
validate.rules.Json.validate(value)
Json.error()
validate.rules.Json.error()
DateRule
class validate.rules.DateRule < Rule
Passes when the value is a valid date string parseable by the date
module (e.g. "2024-06-15", "2024-06-15T12:00:00Z").
DateRule.validate()
validate.rules.DateRule.validate(value)
DateRule.error()
validate.rules.DateRule.error()
After
class validate.rules.After < Rule
Passes when the value is a date string after the given reference date.
Both the value and the reference must be parseable by the date module.
Constructor
validate.rules.After(name, ref)
After.validate()
validate.rules.After.validate(value)
After.error()
validate.rules.After.error()
AfterOrEqual
class validate.rules.AfterOrEqual < Rule
Passes when the value is a date string after or equal to the given reference date.
Constructor
validate.rules.AfterOrEqual(name, ref)
AfterOrEqual.validate()
validate.rules.AfterOrEqual.validate(value)
AfterOrEqual.error()
validate.rules.AfterOrEqual.error()
Before
class validate.rules.Before < Rule
Passes when the value is a date string before the given reference date.
Constructor
validate.rules.Before(name, ref)
Before.validate()
validate.rules.Before.validate(value)
Before.error()
validate.rules.Before.error()
BeforeOrEqual
class validate.rules.BeforeOrEqual < Rule
Passes when the value is a date string before or equal to the given reference date.
Constructor
validate.rules.BeforeOrEqual(name, ref)
BeforeOrEqual.validate()
validate.rules.BeforeOrEqual.validate(value)
BeforeOrEqual.error()
validate.rules.BeforeOrEqual.error()
Timezone
class validate.rules.Timezone < Rule
Passes when the value is a real IANA timezone identifier (e.g. "UTC",
"Africa/Lagos", "America/New_York"), checked against the full
timezone database date.is_valid_timezone() uses.
Timezone.validate()
validate.rules.Timezone.validate(value)
Timezone.error()
validate.rules.Timezone.error()
In
class validate.rules.In < Rule
Passes when the value is contained in the given list of allowed values. Comparison is strict (type-sensitive).
Constructor
validate.rules.In(name, values)
In.validate()
validate.rules.In.validate(value)
In.error()
validate.rules.In.error()
NotIn
class validate.rules.NotIn < Rule
Passes when the value is not contained in the given list of values.
Constructor
validate.rules.NotIn(name, values)
NotIn.validate()
validate.rules.NotIn.validate(value)
NotIn.error()
validate.rules.NotIn.error()
InList
class validate.rules.InList < Rule
Passes when every item in the value list is contained in the allowed list. The field value must be a list.
Constructor
validate.rules.InList(name, values)
InList.validate()
validate.rules.InList.validate(value)
InList.error()
validate.rules.InList.error()
Accepted
class validate.rules.Accepted < Rule
Passes when the value equals one of the specified accepted values:
true, "true", "yes", "on", "1", or 1.
Accepted.validate()
validate.rules.Accepted.validate(value)
Accepted.error()
validate.rules.Accepted.error()
Declined
class validate.rules.Declined < Rule
Passes when the value equals one of the specified declined values:
false, "false", "no", "off", "0", or 0.
Declined.validate()
validate.rules.Declined.validate(value)
Declined.error()
validate.rules.Declined.error()
Equals
class validate.rules.Equals < Rule
Passes when the field value is strictly equal to the given expected value.
Constructor
validate.rules.Equals(name, expected)
Equals.validate()
validate.rules.Equals.validate(value)
Equals.error()
validate.rules.Equals.error()
NotEquals
class validate.rules.NotEquals < Rule
Passes when the field value is not equal to the given forbidden value.
Constructor
validate.rules.NotEquals(name, forbidden)
NotEquals.validate()
validate.rules.NotEquals.validate(value)
NotEquals.error()
validate.rules.NotEquals.error()
Same
class validate.rules.Same < Rule
Passes when the field’s value matches the value of another field in the same data dictionary. Commonly used for password confirmation.
Requires needs_data() == true so that the Schema injects _data.
Example
import validate
var schema = validate.schema({
password: validate.string().min_length(8),
password_confirmation: validate.string().same('password'),
})
Constructor
validate.rules.Same(name, other_field)
Same.needs_data()
validate.rules.Same.needs_data()
Same.validate()
validate.rules.Same.validate(value)
Same.error()
validate.rules.Same.error()
Different
class validate.rules.Different < Rule
Passes when the field’s value does not match the value of another field.
Constructor
validate.rules.Different(name, other_field)
Different.needs_data()
validate.rules.Different.needs_data()
Different.validate()
validate.rules.Different.validate(value)
Different.error()
validate.rules.Different.error()
Confirmed
class validate.rules.Confirmed < Rule
Passes when the field’s value matches {name}_confirmation in the data.
This is the canonical Zuri spelling of Laravel’s confirmed rule.
Confirmed.needs_data()
validate.rules.Confirmed.needs_data()
Confirmed.validate()
validate.rules.Confirmed.validate(value)
Confirmed.error()
validate.rules.Confirmed.error()
RequiredIf
class validate.rules.RequiredIf < Rule
Passes when this field is present and non-nil only if another field in the data satisfies a given condition. If the other field equals any of the given values, this field becomes required.
Example
var rules = validate.schema({
# "shipping_address" is required when "has_delivery" is true
shipping_address: validate.string().required_if('has_delivery', true),
})
Constructor
validate.rules.RequiredIf(name, other_field, other_values)
RequiredIf.needs_data()
validate.rules.RequiredIf.needs_data()
RequiredIf.validate()
validate.rules.RequiredIf.validate(value)
RequiredIf.error()
validate.rules.RequiredIf.error()
RequiredUnless
class validate.rules.RequiredUnless < Rule
Passes when this field is present and non-nil unless another field equals any of the given values.
Constructor
validate.rules.RequiredUnless(name, other_field, other_values)
RequiredUnless.needs_data()
validate.rules.RequiredUnless.needs_data()
RequiredUnless.validate()
validate.rules.RequiredUnless.validate(value)
RequiredUnless.error()
validate.rules.RequiredUnless.error()
RequiredWith
class validate.rules.RequiredWith < Rule
Passes when this field is present and non-nil if any of the listed sibling fields are also present and non-nil.
Constructor
validate.rules.RequiredWith(name, fields)
RequiredWith.needs_data()
validate.rules.RequiredWith.needs_data()
RequiredWith.validate()
validate.rules.RequiredWith.validate(value)
RequiredWith.error()
validate.rules.RequiredWith.error()
RequiredWithAll
class validate.rules.RequiredWithAll < Rule
Passes when this field is present and non-nil if all of the listed sibling fields are present and non-nil.
Constructor
validate.rules.RequiredWithAll(name, fields)
RequiredWithAll.needs_data()
validate.rules.RequiredWithAll.needs_data()
RequiredWithAll.validate()
validate.rules.RequiredWithAll.validate(value)
RequiredWithAll.error()
validate.rules.RequiredWithAll.error()
RequiredWithout
class validate.rules.RequiredWithout < Rule
Passes when this field is present and non-nil if any of the listed sibling fields are absent or nil.
Constructor
validate.rules.RequiredWithout(name, fields)
RequiredWithout.needs_data()
validate.rules.RequiredWithout.needs_data()
RequiredWithout.validate()
validate.rules.RequiredWithout.validate(value)
RequiredWithout.error()
validate.rules.RequiredWithout.error()
RequiredWithoutAll
class validate.rules.RequiredWithoutAll < Rule
Passes when this field is present and non-nil if all of the listed sibling fields are absent or nil.
Constructor
validate.rules.RequiredWithoutAll(name, fields)
RequiredWithoutAll.needs_data()
validate.rules.RequiredWithoutAll.needs_data()
RequiredWithoutAll.validate()
validate.rules.RequiredWithoutAll.validate(value)
RequiredWithoutAll.error()
validate.rules.RequiredWithoutAll.error()
Prohibits
class validate.rules.Prohibits < Rule
Passes when both this field and the listed sibling fields are either all present (non-nil) or all absent (nil / blank).
Constructor
validate.rules.Prohibits(name, fields)
Prohibits.needs_data()
validate.rules.Prohibits.needs_data()
Prohibits.validate()
validate.rules.Prohibits.validate(value)
Prohibits.error()
validate.rules.Prohibits.error()
Prohibited
class validate.rules.Prohibited < Rule
Passes when this field is absent or nil. Useful to explicitly forbid a field from being submitted.
Prohibited.validate()
validate.rules.Prohibited.validate(value)
Prohibited.error()
validate.rules.Prohibited.error()
ProhibitedIf
class validate.rules.ProhibitedIf < Rule
Passes when this field is absent or nil if another field equals one of the given values.
Constructor
validate.rules.ProhibitedIf(name, other_field, other_values)
ProhibitedIf.needs_data()
validate.rules.ProhibitedIf.needs_data()
ProhibitedIf.validate()
validate.rules.ProhibitedIf.validate(value)
ProhibitedIf.error()
validate.rules.ProhibitedIf.error()
ProhibitedUnless
class validate.rules.ProhibitedUnless < Rule
Passes when this field is absent or nil unless another field equals one of the given values.
Constructor
validate.rules.ProhibitedUnless(name, other_field, other_values)
ProhibitedUnless.needs_data()
validate.rules.ProhibitedUnless.needs_data()
ProhibitedUnless.validate()
validate.rules.ProhibitedUnless.validate(value)
ProhibitedUnless.error()
validate.rules.ProhibitedUnless.error()
Distinct
class validate.rules.Distinct < Rule
Passes when the list field contains no duplicate values.
Distinct.validate()
validate.rules.Distinct.validate(value)
Distinct.error()
validate.rules.Distinct.error()
MinItems
class validate.rules.MinItems < Rule
Passes when the list field has at least min items.
Constructor
validate.rules.MinItems(name, min)
MinItems.validate()
validate.rules.MinItems.validate(value)
MinItems.error()
validate.rules.MinItems.error()
MaxItems
class validate.rules.MaxItems < Rule
Passes when the list field has at most max items.
Constructor
validate.rules.MaxItems(name, max)
MaxItems.validate()
validate.rules.MaxItems.validate(value)
MaxItems.error()
validate.rules.MaxItems.error()
Each
class validate.rules.Each < Rule
Passes when every item in the list satisfies the given Validator
chain.
Example
import validate
var schema = validate.schema({
tags: validate.list().each(validate.string().max_length(32)),
})
Constructor
validate.rules.Each(name, validator)
Each.validate()
validate.rules.Each.validate(value)
Each.error()
validate.rules.Each.error()
Nil
class validate.rules.Nil < Rule
Passes when the value is nil. Useful for asserting a field is absent.
Nil.validate()
validate.rules.Nil.validate(value)
Nil.error()
validate.rules.Nil.error()
NotNil
class validate.rules.NotNil < Rule
Passes when the value is not nil.
NotNil.validate()
validate.rules.NotNil.validate(value)
NotNil.error()
validate.rules.NotNil.error()
CustomRule
class validate.rules.CustomRule < Rule
Custom rule backed by a caller-supplied function.
Used internally by Validator.custom(). The function receives the field
value and must return true to pass.
Constructor
validate.rules.CustomRule(name, fn, message)
CustomRule.validate()
validate.rules.CustomRule.validate(value)
CustomRule.error()
validate.rules.CustomRule.error()
CustomRuleWithData
class validate.rules.CustomRuleWithData < Rule
Custom rule that also receives the full data dictionary.
Used internally by Validator.custom_with_data(). The function receives
(value, data) and must return true to pass.
Constructor
validate.rules.CustomRuleWithData(name, fn, message)
CustomRuleWithData.needs_data()
validate.rules.CustomRuleWithData.needs_data()
CustomRuleWithData.validate()
validate.rules.CustomRuleWithData.validate(value)
CustomRuleWithData.error()
validate.rules.CustomRuleWithData.error()
validate.schema
import validate.schema
validatelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledvalidate.schema.*needsimport validate.schema.
validate.schema
Provides the Schema class which binds a ruleset dictionary to data and
produces a structured validation result.
Features
- Dot-notation keys:
"address.city"resolves nested dictionaries. - Wildcard keys:
"tags.*"validates every item in a list against aValidatorchain. - Bail-on-first-error per field (default) or collect-all-errors mode.
- Sometimes: skip rules when a field is absent.
- Nullable: treat nil as passing for all subsequent rules.
- Label override: substitute a human-readable name in error messages.
- Required-field checking: fields with
required()that are entirely absent from the data dict are caught and reported. check(data): returns{ valid: bool, errors: list }.check_or_raise(data): raisesValidationErroron failure.
Classes
ValidationError
class validate.ValidationError < Error
Raised by Schema.check_or_raise() when validation fails.
The errors property holds the same list of { field, message } dicts
that Schema.check() returns under the errors key.
catch {
schema.check_or_raise(req.body)
} as e
if e {
echo e.message # "Validation failed"
echo e.errors # [{ field: 'email', message: '...' }, ...]
}
Fields
| Field | Type | Description |
|---|---|---|
errors | List of { field: string, message: string } dictionaries, one per failed rule. |
Constructor
validate.ValidationError(errors)
Parameters
errors(list) — Validation error list fromSchema.check().
Schema
class validate.Schema
Binds a ruleset to data and validates it.
Basic usage
import validate
var schema = validate.schema({
name: validate.required().string().max_length(100),
email: validate.required().string().email(),
age: validate.required().integer().gte(18),
})
var result = schema.check({
name: 'Ada Lovelace',
email: 'ada@example.com',
age: 36,
})
echo result.valid # true
echo result.errors # []
Nested field validation (dot-notation)
var schema = validate.schema({
'address.city': validate.required().string(),
'address.country': validate.required().string().length(2).uppercase(),
})
schema.check({
address: { city: 'Lagos', country: 'NG' }
})
Wildcard list validation
var schema = validate.schema({
'tags.*': validate.required().string().max_length(32),
})
schema.check({ tags: ['zuri', 'backend', 'fast'] })
Raising on failure
catch {
schema.check_or_raise(data)
} as e
if e {
# e.errors is a list of { field, message } dicts
for err in e.errors {
echo '${err.field}: ${err.message}'
}
}
schema.check_or_raise(data)
Grouped errors (errors keyed by field)
var result = schema.check(data)
var grouped = schema.group_errors(result.errors)
# { email: ['must be valid email'], age: ['must be >= 18'] }
Constructor
validate.Schema(ruleset)
Parameters
ruleset(dict) — Map of field keys toValidatorinstances.
Raises ArgumentError When ruleset is not a dictionary.
Raises ValueError When a key is not a string or a value is not a
Validator.
Schema.check()
validate.Schema.check(data) -> dict
Validates data against the schema and returns a result dictionary.
The result always has the shape:
{
valid: bool,
errors: list<{ field: string, message: string }>
}
Fields present in the data but absent from the schema are silently
ignored. Fields present in the schema but absent from the data are
validated against their rules (the required rule catches absent
fields; all other rules skip nil values unless chained after
required).
Parameters
data(dict)
Returns dict
Raises ArgumentError
Schema.check_or_raise()
validate.Schema.check_or_raise(data) -> dict
Validates data and raises a ValidationError if validation fails.
Returns the result dictionary on success.
def create_user(req, res) {
catch {
schema.check_or_raise(req.body)
} as e
if e {
return res.json({ errors: e.errors }, 422)
}
return res.json({ ok: true })
}
Parameters
data(dict)
Returns dict
Raises ValidationError
Raises ArgumentError
Schema.group_errors()
validate.Schema.group_errors(errors) -> dict
Converts a flat errors list (as returned by check) into a dictionary
keyed by field name, where each value is a list of error message
strings.
var result = schema.check(data)
var grouped = schema.group_errors(result.errors)
# { 'email': ['must be a valid email address'], 'age': ['must be >= 18'] }
Parameters
errors(list) — Theerrorslist from acheck()result.
Returns dict
Schema.first_error()
validate.Schema.first_error(errors, field_key) -> string|nil
Returns the first error message for the given field, or nil when that field has no errors in the provided errors list.
var result = schema.check(data)
var msg = schema.first_error(result.errors, 'email')
Parameters
errors(list) — Theerrorslist from acheck()result.field_key(string) — The field to look up.
Returns string|nil
Schema.field_errors()
validate.Schema.field_errors(errors, field_key) -> list<string>
Returns all error messages for the given field as a list, or an empty list when that field has no errors.
Parameters
errors(list)field_key(string)
Returns list<string>
Schema.has_error()
validate.Schema.has_error(errors, field_key) -> bool
Returns true when the given field has at least one error in the
provided errors list.
Parameters
errors(list)field_key(string)
Returns bool
Schema.ruleset()
validate.Schema.ruleset() -> dict
Returns a copy of this schema’s own ruleset dictionary (field key →
Validator). A copy, not a live reference, so mutating the result can
never affect this Schema itself.
Returns dict
Schema.extend()
validate.Schema.extend(other) -> Schema
Extends this schema with additional rules from another schema or a plain
ruleset dictionary, returning a new Schema instance. Rules in other
override rules for the same field key.
var base = validate.schema({ name: validate.required().string() })
var extended = base.extend({
email: validate.required().string().email(),
})
Parameters
other(dict|Schema)
Returns Schema
Schema.only()
validate.Schema.only(keys) -> Schema
Returns a new Schema containing only the rules for the given field
keys.
var partial = full_schema.only(['name', 'email'])
Parameters
keys(list)
Returns Schema
Schema.except()
validate.Schema.except(keys) -> Schema
Returns a new Schema with the rules for the given field keys removed.
var without_admin = schema.except(['role', 'is_superuser'])
Parameters
keys(list)
Returns Schema
validate.validator
import validate.validator
validatelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledvalidate.validator.*needsimport validate.validator.
validate.validator
Provides the Validator fluent builder. Each method appends one rule
class (and its constructor arguments) to an internal chain. The chain is
later resolved by Schema: it instantiates each rule with the field
name injected, then calls validate(value) in order.
Rules stop executing for a field the moment one fails, unless
bail(false) has been explicitly disabled. When sometimes() is set
and the field is absent or nil, the entire rule chain is skipped.
Classes
Validator
class validate.Validator
Fluent rule chain builder for a single field.
Every method returns self so calls can be chained:
validate.string().email().max_length(254).required()
The order of chained rules is the order in which they are evaluated. Put
required() first so that a missing value is caught before
type-specific rules run against a nil.
Custom rules
Provide an inline anonymous function via custom():
validate.string().custom(@(value) {
return value.starts_with('zuri-')
}, 'Must start with "zuri-".')
Or subclass Rule and add it via use():
import validate { Rule, Validator }
class Palindrome < Rule {
validate(value) {
if !is_string(value) return false
var s = value.lower()
return s == s.reverse()
}
error() { return '${self.name} must be a palindrome.' }
}
var v = Validator().string().use(Palindrome)
Validator.label()
validate.Validator.label(label) -> Validator
Sets a human-readable label for this field. When set, error messages
substitute label for the raw field key.
validate.string().label('Email address').email()
# → "The Email address field must be a valid email address."
Parameters
label(string)
Returns Validator
Validator.sometimes()
validate.Validator.sometimes() -> Validator
Skips the entire rule chain when the field is absent from the data dictionary or its value is nil / blank. Use this to mark fields as optional while still validating them when they are present.
var rules = validate.schema({
# "bio" is optional; when given it must be at most 500 chars.
bio: validate.string().max_length(500).sometimes(),
})
Returns Validator
Validator.bail()
validate.Validator.bail(enabled) -> Validator
Controls whether validation stops at the first failing rule for this
field. Defaults to true. Set to false to collect all errors.
Parameters
enabled(bool) — Passfalseto disable early stopping.
Returns Validator
Validator.required()
validate.Validator.required() -> Validator
The field must be present, non-nil, and non-empty.
Returns Validator
Validator.nullable()
validate.Validator.nullable() -> Validator
Marks the field as nullable. A nil value will pass all subsequent rules
without evaluation. Combine with sometimes() for a fully
optional-nullable field.
Returns Validator
Validator.string()
validate.Validator.string() -> Validator
The field value must be a string.
Returns Validator
Validator.number()
validate.Validator.number() -> Validator
The field value must be a number (integer or float).
Returns Validator
Validator.integer()
validate.Validator.integer() -> Validator
The field value must be an integer (no fractional part). Also accepts string representations of integers.
Returns Validator
Validator.boolean()
validate.Validator.boolean() -> Validator
The field value must be boolean or a boolean-like string/number.
Accepts: true, false, "true", "false", "1", "0", 1, 0.
Returns Validator
Validator.list()
validate.Validator.list() -> Validator
The field value must be a list.
Returns Validator
Validator.dict()
validate.Validator.dict() -> Validator
The field value must be a dictionary.
Returns Validator
Validator.numeric()
validate.Validator.numeric() -> Validator
The field value must be numeric: either a number type, or a string that converts cleanly to a number.
Returns Validator
Validator.size()
validate.Validator.size(n) -> Validator
The value’s size must equal n.
Size is: character count for strings, item count for lists/dicts, and numeric value for numbers.
Parameters
n(number)
Returns Validator
Validator.min()
validate.Validator.min(min) -> Validator
The value’s size must be at least min.
Parameters
min(number)
Returns Validator
Validator.max()
validate.Validator.max(max) -> Validator
The value’s size must be at most max.
Parameters
max(number)
Returns Validator
Validator.between()
validate.Validator.between(min, max) -> Validator
The value’s size must be between min and max (inclusive).
Parameters
min(number)max(number)
Returns Validator
Validator.length()
validate.Validator.length(n) -> Validator
The string’s character count must equal exactly n.
Parameters
n(number)
Returns Validator
Validator.min_length()
validate.Validator.min_length(min) -> Validator
The string must be at least min characters long.
Parameters
min(number)
Returns Validator
Validator.max_length()
validate.Validator.max_length(max) -> Validator
The string must be at most max characters long.
Parameters
max(number)
Returns Validator
Validator.length_between()
validate.Validator.length_between(min, max) -> Validator
The string’s length must be between min and max characters
(inclusive).
Parameters
min(number)max(number)
Returns Validator
Validator.gt()
validate.Validator.gt(n) -> Validator
The numeric value must be greater than n.
Parameters
n(number)
Returns Validator
Validator.gte()
validate.Validator.gte(n) -> Validator
The numeric value must be greater than or equal to n.
Parameters
n(number)
Returns Validator
Validator.lt()
validate.Validator.lt(n) -> Validator
The numeric value must be less than n.
Parameters
n(number)
Returns Validator
Validator.lte()
validate.Validator.lte(n) -> Validator
The numeric value must be less than or equal to n.
Parameters
n(number)
Returns Validator
Validator.positive()
validate.Validator.positive() -> Validator
The numeric value must be strictly positive (greater than zero).
Returns Validator
Validator.negative()
validate.Validator.negative() -> Validator
The numeric value must be strictly negative (less than zero).
Returns Validator
Validator.positive_or_zero()
validate.Validator.positive_or_zero() -> Validator
The numeric value must be zero or positive.
Returns Validator
Validator.negative_or_zero()
validate.Validator.negative_or_zero() -> Validator
The numeric value must be zero or negative.
Returns Validator
Validator.multiple_of()
validate.Validator.multiple_of(n) -> Validator
The numeric value must be a multiple of n.
Parameters
n(number)
Returns Validator
Validator.alpha()
validate.Validator.alpha() -> Validator
The string must contain only ASCII alphabetic characters (a-z, A-Z).
Returns Validator
Validator.alpha_num()
validate.Validator.alpha_num() -> Validator
The string must contain only ASCII alphanumeric characters.
Returns Validator
Validator.alpha_dash()
validate.Validator.alpha_dash() -> Validator
The string must contain only ASCII alphanumeric characters, hyphens, and underscores.
Returns Validator
Validator.starts_with()
validate.Validator.starts_with(prefixes) -> Validator
The string must start with one of the given prefixes.
Parameters
prefixes(string|list)
Returns Validator
Validator.doesnt_start_with()
validate.Validator.doesnt_start_with(prefixes) -> Validator
The string must not start with any of the given prefixes.
Parameters
prefixes(string|list)
Returns Validator
Validator.ends_with()
validate.Validator.ends_with(suffixes) -> Validator
The string must end with one of the given suffixes.
Parameters
suffixes(string|list)
Returns Validator
Validator.doesnt_end_with()
validate.Validator.doesnt_end_with(suffixes) -> Validator
The string must not end with any of the given suffixes.
Parameters
suffixes(string|list)
Returns Validator
Validator.contains()
validate.Validator.contains(needle) -> Validator
The string must contain the given substring.
Parameters
needle(string)
Returns Validator
Validator.doesnt_contain()
validate.Validator.doesnt_contain(needle) -> Validator
The string must not contain the given substring.
Parameters
needle(string)
Returns Validator
Validator.regex()
validate.Validator.regex(pattern) -> Validator
The string must match the given regular expression pattern.
Parameters
pattern(string)
Returns Validator
Validator.not_regex()
validate.Validator.not_regex(pattern) -> Validator
The string must not match the given regular expression pattern.
Parameters
pattern(string)
Returns Validator
Validator.lowercase()
validate.Validator.lowercase() -> Validator
The string must be entirely lowercase.
Returns Validator
Validator.uppercase()
validate.Validator.uppercase() -> Validator
The string must be entirely uppercase.
Returns Validator
Validator.not_blank()
validate.Validator.not_blank() -> Validator
The string must not consist entirely of whitespace characters.
Returns Validator
Validator.email()
validate.Validator.email() -> Validator
The value must be a syntactically valid email address.
Returns Validator
Validator.url()
validate.Validator.url() -> Validator
The value must be a valid HTTP or HTTPS URL.
Returns Validator
Validator.ipv4()
validate.Validator.ipv4() -> Validator
The value must be a valid IPv4 address.
Returns Validator
Validator.ipv6()
validate.Validator.ipv6() -> Validator
The value must be a valid IPv6 address.
Returns Validator
Validator.ip()
validate.Validator.ip() -> Validator
The value must be a valid IPv4 or IPv6 address.
Returns Validator
Validator.uuid()
validate.Validator.uuid() -> Validator
The value must be a valid canonical UUID
(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
Returns Validator
Validator.json()
validate.Validator.json() -> Validator
The value must be a valid JSON string.
Returns Validator
Validator.date()
validate.Validator.date() -> Validator
The value must be a valid date string parseable by the date module.
Returns Validator
Validator.after()
validate.Validator.after(ref) -> Validator
The value must be a date after ref.
Parameters
ref(string) — A date string parseable by thedatemodule.
Returns Validator
Validator.after_or_equal()
validate.Validator.after_or_equal(ref) -> Validator
The value must be a date after or equal to ref.
Parameters
ref(string)
Returns Validator
Validator.before()
validate.Validator.before(ref) -> Validator
The value must be a date before ref.
Parameters
ref(string)
Returns Validator
Validator.before_or_equal()
validate.Validator.before_or_equal(ref) -> Validator
The value must be a date before or equal to ref.
Parameters
ref(string)
Returns Validator
Validator.timezone()
validate.Validator.timezone() -> Validator
The value must be a valid timezone identifier recognised by the date
module (e.g. "UTC", "Africa/Lagos").
Returns Validator
Validator.is_in()
validate.Validator.is_in(values) -> Validator
The value must be one of the given allowed values (strict comparison).
Parameters
values(list)
Returns Validator
Validator.not_in()
validate.Validator.not_in(values) -> Validator
The value must not be one of the given forbidden values.
Parameters
values(list)
Returns Validator
Validator.in_list()
validate.Validator.in_list(values) -> Validator
Every item in the list value must be contained in values.
Parameters
values(list)
Returns Validator
Validator.accepted()
validate.Validator.accepted() -> Validator
The value must be one of the accepted truthy representations: true,
"true", "yes", "on", "1", 1.
Returns Validator
Validator.declined()
validate.Validator.declined() -> Validator
The value must be one of the declined falsy representations: false,
"false", "no", "off", "0", 0.
Returns Validator
Validator.equals()
validate.Validator.equals(expected) -> Validator
The value must strictly equal expected.
Parameters
expected(any)
Returns Validator
Validator.not_equals()
validate.Validator.not_equals(forbidden) -> Validator
The value must not equal forbidden.
Parameters
forbidden(any)
Returns Validator
Validator.same()
validate.Validator.same(other_field) -> Validator
The value must match the value of other_field in the same data
dictionary. Commonly used to confirm passwords.
Parameters
other_field(string)
Returns Validator
Validator.different()
validate.Validator.different(other_field) -> Validator
The value must differ from the value of other_field.
Parameters
other_field(string)
Returns Validator
Validator.confirmed()
validate.Validator.confirmed() -> Validator
The value must match the {field_name}_confirmation sibling field.
Returns Validator
Validator.required_if()
validate.Validator.required_if(other_field, other_values) -> Validator
The field becomes required when other_field equals any value in
other_values.
Parameters
other_field(string)other_values(any|list)
Returns Validator
Validator.required_unless()
validate.Validator.required_unless(other_field, other_values) -> Validator
The field becomes required unless other_field equals any value in
other_values.
Parameters
other_field(string)other_values(any|list)
Returns Validator
Validator.required_with()
validate.Validator.required_with(fields) -> Validator
The field becomes required when any of the listed sibling fields are present and non-blank.
Parameters
fields(string|list)
Returns Validator
Validator.required_with_all()
validate.Validator.required_with_all(fields) -> Validator
The field becomes required when all of the listed sibling fields are present and non-blank.
Parameters
fields(string|list)
Returns Validator
Validator.required_without()
validate.Validator.required_without(fields) -> Validator
The field becomes required when any of the listed sibling fields are absent or blank.
Parameters
fields(string|list)
Returns Validator
Validator.required_without_all()
validate.Validator.required_without_all(fields) -> Validator
The field becomes required when all of the listed sibling fields are absent or blank.
Parameters
fields(string|list)
Returns Validator
Validator.prohibits()
validate.Validator.prohibits(fields) -> Validator
When this field is present, none of the listed sibling fields may also be present.
Parameters
fields(string|list)
Returns Validator
Validator.prohibited()
validate.Validator.prohibited() -> Validator
This field must be absent or nil: it is never allowed.
Returns Validator
Validator.prohibited_if()
validate.Validator.prohibited_if(other_field, other_values) -> Validator
The field must be absent when other_field equals any of
other_values.
Parameters
other_field(string)other_values(any|list)
Returns Validator
Validator.prohibited_unless()
validate.Validator.prohibited_unless(other_field, other_values) -> Validator
The field must be absent unless other_field equals any of
other_values.
Parameters
other_field(string)other_values(any|list)
Returns Validator
Validator.distinct()
validate.Validator.distinct() -> Validator
The list must not contain duplicate values.
Returns Validator
Validator.min_items()
validate.Validator.min_items(min) -> Validator
The list must have at least min items.
Parameters
min(number)
Returns Validator
Validator.max_items()
validate.Validator.max_items(max) -> Validator
The list must have at most max items.
Parameters
max(number)
Returns Validator
Validator.each()
validate.Validator.each(validator) -> Validator
Every item in the list must pass the given Validator chain.
var rules = validate.schema({
# A list of between 1 and 10 valid email strings.
recipients: validate.list()
.min_items(1).max_items(10)
.each(validate.string().email()),
})
Parameters
validator(Validator)
Returns Validator
Validator.is_nil()
validate.Validator.is_nil() -> Validator
The value must be nil.
Returns Validator
Validator.not_nil()
validate.Validator.not_nil() -> Validator
The value must not be nil.
Returns Validator
Validator.custom()
validate.Validator.custom(fn, message) -> Validator
Adds a custom inline validation rule backed by an anonymous function.
The function receives the field value and must return true to pass. An
optional message overrides the default error text.
var v = validate.string().custom(@(value) {
return value.starts_with('zuri-')
}, 'Must be a valid Zuri identifier.')
Parameters
fn(function) —@(value) -> boolmessage(string) — Optional custom error message.
Returns Validator
Validator.custom_with_data()
validate.Validator.custom_with_data(fn, message) -> Validator
Adds a custom inline rule whose function also receives the full data dictionary, allowing cross-field validation logic.
var v = validate.number().custom_with_data(@(value, data) {
return value < data.get('max_price', 0)
}, 'Must be less than max_price.')
Parameters
fn(function) —@(value, data) -> boolmessage(string) — Optional custom error message.
Returns Validator
Validator.use()
validate.Validator.use(rule_class) -> Validator
Adds a pre-constructed Rule subclass (the class itself, not an
instance) to the chain. Use this to attach custom Rule subclasses that
require no constructor arguments beyond the field name.
import validate { Rule, Validator }
class Palindrome < Rule {
validate(value) {
return value == ''.join(value.to_list().reverse())
}
error() {
return '${self.name} must be a palindrome.'
}
}
var v = Validator().string().use(Palindrome)
Parameters
rule_class(class) — A class that extendsRule.
Returns Validator
Validator.bail_enabled()
validate.Validator.bail_enabled() -> bool
Whether validation should stop at the first failing rule for this field
(see bail()).
Returns bool
Validator.is_sometimes()
validate.Validator.is_sometimes() -> bool
Whether this field’s whole chain should be skipped when the field is
absent or blank (see sometimes()).
Returns bool
Validator.get_label()
validate.Validator.get_label() -> ?string
This field’s human-readable label override, or nil if none was set
(see label()).
Returns ?string
validate.validators
import validate
Everything here is re-exported by
validate, soimport validateis enough and the names are called asvalidate.*. Importingvalidate.validatorson its own works too and reaches the same definitions.
Top-level convenience functions that create a fresh Validator instance
with one rule pre-applied. Every rule exposed by the Validator class
has a corresponding function here so callers never need to import
Validator directly for common cases.
Every function returns a Validator, so additional rules can be chained
immediately:
import validate
validate.required().string().email().max_length(254)
validate.integer().gte(0).lte(150)
validate.list().min_items(1).each(validate.string())
validate.string().starts_with('ZURI-').length(12)
Functions
value()
validate.value() -> Validator
Returns a bare Validator with no rules pre-applied. Useful when you
want to compose rules manually or start with a custom rule.
Returns Validator
required()
validate.required() -> Validator
The field must be present, non-nil, and non-empty.
Returns Validator
nullable()
validate.nullable() -> Validator
A nil value passes all subsequent rules without evaluation. Combine with
sometimes() for a fully optional-nullable field.
Returns Validator
sometimes()
validate.sometimes() -> Validator
Skips the entire rule chain when the field is absent from the data dictionary or its value is nil / blank. Use this to mark fields as optional while still validating them when they are present.
Returns Validator
string()
validate.string() -> Validator
The value must be a string.
Returns Validator
number()
validate.number() -> Validator
The value must be a number (integer or float).
Returns Validator
integer()
validate.integer() -> Validator
The value must be an integer (no fractional part). Also accepts string representations of integers.
Returns Validator
boolean()
validate.boolean() -> Validator
The value must be boolean-like: true, false, "true", "false",
"1", "0", 1, or 0.
Returns Validator
list()
validate.list() -> Validator
The value must be a list.
Returns Validator
dict()
validate.dict() -> Validator
The value must be a dictionary.
Returns Validator
numeric()
validate.numeric() -> Validator
The value must be numeric: either a number type or a string that converts cleanly to a number.
Returns Validator
size()
validate.size(n) -> Validator
The value’s size must equal exactly n.
Size is: character count for strings, item count for lists/dicts, and numeric value for numbers.
Parameters
n(number)
Returns Validator
min()
validate.min(min) -> Validator
The value’s size must be at least min.
Parameters
min(number)
Returns Validator
max()
validate.max(max) -> Validator
The value’s size must be at most max.
Parameters
max(number)
Returns Validator
between()
validate.between(min, max) -> Validator
The value’s size must be between min and max (inclusive).
Parameters
min(number)max(number)
Returns Validator
length()
validate.length(n) -> Validator
The string’s character count must equal exactly n.
Parameters
n(number)
Returns Validator
min_length()
validate.min_length(min) -> Validator
The string must be at least min characters long.
Parameters
min(number)
Returns Validator
max_length()
validate.max_length(max) -> Validator
The string must be at most max characters long.
Parameters
max(number)
Returns Validator
length_between()
validate.length_between(min, max) -> Validator
The string’s length must be between min and max characters
(inclusive).
Parameters
min(number)max(number)
Returns Validator
gt()
validate.gt(n) -> Validator
The numeric value must be greater than n.
Parameters
n(number)
Returns Validator
gte()
validate.gte(n) -> Validator
The numeric value must be greater than or equal to n.
Parameters
n(number)
Returns Validator
lt()
validate.lt(n) -> Validator
The numeric value must be less than n.
Parameters
n(number)
Returns Validator
lte()
validate.lte(n) -> Validator
The numeric value must be less than or equal to n.
Parameters
n(number)
Returns Validator
positive()
validate.positive() -> Validator
The numeric value must be strictly positive (greater than zero).
Returns Validator
negative()
validate.negative() -> Validator
The numeric value must be strictly negative (less than zero).
Returns Validator
positive_or_zero()
validate.positive_or_zero() -> Validator
The numeric value must be zero or positive.
Returns Validator
negative_or_zero()
validate.negative_or_zero() -> Validator
The numeric value must be zero or negative.
Returns Validator
multiple_of()
validate.multiple_of(n) -> Validator
The numeric value must be a multiple of n.
Parameters
n(number)
Returns Validator
alpha()
validate.alpha() -> Validator
The string must contain only ASCII alphabetic characters (a-z, A-Z).
Returns Validator
alpha_num()
validate.alpha_num() -> Validator
The string must contain only ASCII alphanumeric characters.
Returns Validator
alpha_dash()
validate.alpha_dash() -> Validator
The string must contain only ASCII alphanumeric characters, hyphens, and underscores.
Returns Validator
starts_with()
validate.starts_with(prefixes) -> Validator
The string must start with one of the given prefixes.
Parameters
prefixes(string|list)
Returns Validator
doesnt_start_with()
validate.doesnt_start_with(prefixes) -> Validator
The string must not start with any of the given prefixes.
Parameters
prefixes(string|list)
Returns Validator
ends_with()
validate.ends_with(suffixes) -> Validator
The string must end with one of the given suffixes.
Parameters
suffixes(string|list)
Returns Validator
doesnt_end_with()
validate.doesnt_end_with(suffixes) -> Validator
The string must not end with any of the given suffixes.
Parameters
suffixes(string|list)
Returns Validator
contains()
validate.contains(needle) -> Validator
The string must contain the given substring.
Parameters
needle(string)
Returns Validator
doesnt_contain()
validate.doesnt_contain(needle) -> Validator
The string must not contain the given substring.
Parameters
needle(string)
Returns Validator
regex()
validate.regex(pattern) -> Validator
The string must match the given regular expression pattern.
Parameters
pattern(string)
Returns Validator
not_regex()
validate.not_regex(pattern) -> Validator
The string must not match the given regular expression pattern.
Parameters
pattern(string)
Returns Validator
lowercase()
validate.lowercase() -> Validator
The string must be entirely lowercase.
Returns Validator
uppercase()
validate.uppercase() -> Validator
The string must be entirely uppercase.
Returns Validator
not_blank()
validate.not_blank() -> Validator
The string must not consist entirely of whitespace.
Returns Validator
email()
validate.email() -> Validator
The value must be a syntactically valid email address.
Returns Validator
url()
validate.url() -> Validator
The value must be a valid HTTP or HTTPS URL.
Returns Validator
ipv4()
validate.ipv4() -> Validator
The value must be a valid IPv4 address.
Returns Validator
ipv6()
validate.ipv6() -> Validator
The value must be a valid IPv6 address.
Returns Validator
ip()
validate.ip() -> Validator
The value must be a valid IPv4 or IPv6 address.
Returns Validator
uuid()
validate.uuid() -> Validator
The value must be a valid canonical UUID
(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
Returns Validator
json()
validate.json() -> Validator
The value must be a valid JSON string.
Returns Validator
date()
validate.date() -> Validator
The value must be a valid date string parseable by the date module.
Returns Validator
after()
validate.after(ref) -> Validator
The value must be a date string after ref.
Parameters
ref(string) — A date string parseable by thedatemodule.
Returns Validator
after_or_equal()
validate.after_or_equal(ref) -> Validator
The value must be a date string after or equal to ref.
Parameters
ref(string)
Returns Validator
before()
validate.before(ref) -> Validator
The value must be a date string before ref.
Parameters
ref(string)
Returns Validator
before_or_equal()
validate.before_or_equal(ref) -> Validator
The value must be a date string before or equal to ref.
Parameters
ref(string)
Returns Validator
timezone()
validate.timezone() -> Validator
The value must be a valid timezone identifier recognised by the date
module (e.g. "UTC", "Africa/Lagos", "America/New_York").
Returns Validator
is_in()
validate.is_in(values) -> Validator
The value must be one of the given allowed values (strict comparison).
Parameters
values(list)
Returns Validator
not_in()
validate.not_in(values) -> Validator
The value must not be one of the given forbidden values.
Parameters
values(list)
Returns Validator
in_list()
validate.in_list(values) -> Validator
Every item in the list value must be contained in values.
Parameters
values(list)
Returns Validator
accepted()
validate.accepted() -> Validator
The value must be one of the accepted truthy representations: true,
"true", "yes", "on", "1", 1.
Returns Validator
declined()
validate.declined() -> Validator
The value must be one of the declined falsy representations: false,
"false", "no", "off", "0", 0.
Returns Validator
equals()
validate.equals(expected) -> Validator
The value must strictly equal expected.
Parameters
expected(any)
Returns Validator
not_equals()
validate.not_equals(forbidden) -> Validator
The value must not equal forbidden.
Parameters
forbidden(any)
Returns Validator
same()
validate.same(other_field) -> Validator
The value must match the value of other_field in the same data
dictionary. Commonly used to confirm passwords.
Parameters
other_field(string)
Returns Validator
different()
validate.different(other_field) -> Validator
The value must differ from the value of other_field.
Parameters
other_field(string)
Returns Validator
confirmed()
validate.confirmed() -> Validator
The value must match the {field_name}_confirmation sibling field.
Returns Validator
required_if()
validate.required_if(other_field, other_values) -> Validator
The field becomes required when other_field equals any value in
other_values.
Parameters
other_field(string)other_values(any|list)
Returns Validator
required_unless()
validate.required_unless(other_field, other_values) -> Validator
The field becomes required unless other_field equals any value in
other_values.
Parameters
other_field(string)other_values(any|list)
Returns Validator
required_with()
validate.required_with(fields) -> Validator
The field becomes required when any of the listed sibling fields are present and non-blank.
Parameters
fields(string|list)
Returns Validator
required_with_all()
validate.required_with_all(fields) -> Validator
The field becomes required when all of the listed sibling fields are present and non-blank.
Parameters
fields(string|list)
Returns Validator
required_without()
validate.required_without(fields) -> Validator
The field becomes required when any of the listed sibling fields are absent or blank.
Parameters
fields(string|list)
Returns Validator
required_without_all()
validate.required_without_all(fields) -> Validator
The field becomes required when all of the listed sibling fields are absent or blank.
Parameters
fields(string|list)
Returns Validator
prohibits()
validate.prohibits(fields) -> Validator
When this field is present, none of the listed sibling fields may also be present.
Parameters
fields(string|list)
Returns Validator
prohibited()
validate.prohibited() -> Validator
This field must be absent or nil: it is never permitted.
Returns Validator
prohibited_if()
validate.prohibited_if(other_field, other_values) -> Validator
The field must be absent when other_field equals any of
other_values.
Parameters
other_field(string)other_values(any|list)
Returns Validator
prohibited_unless()
validate.prohibited_unless(other_field, other_values) -> Validator
The field must be absent unless other_field equals any of
other_values.
Parameters
other_field(string)other_values(any|list)
Returns Validator
distinct()
validate.distinct() -> Validator
The list must not contain duplicate values.
Returns Validator
min_items()
validate.min_items(min) -> Validator
The list must have at least min items.
Parameters
min(number)
Returns Validator
max_items()
validate.max_items(max) -> Validator
The list must have at most max items.
Parameters
max(number)
Returns Validator
each()
validate.each(validator) -> Validator
Every item in the list must pass the given Validator chain.
validate.each(validate.string().email())
# Equivalent to: validate.list().each(validate.string().email())
Parameters
validator(Validator)
Returns Validator
is_nil()
validate.is_nil() -> Validator
The value must be nil.
Returns Validator
not_nil()
validate.not_nil() -> Validator
The value must not be nil.
Returns Validator
custom()
validate.custom(fn, message) -> Validator
Validates with an inline anonymous function. The function receives the
field value and must return true to pass.
validate.custom(@(value) {
return value.starts_with('ZURI-')
}, 'Must be a valid Zuri identifier.')
Parameters
fn(function) —@(value) -> boolmessage(string) — Optional custom error message.
Returns Validator
custom_with_data()
validate.custom_with_data(fn, message) -> Validator
Validates with an inline function that also receives the full data
dictionary, enabling cross-field logic without subclassing Rule.
validate.custom_with_data(@(value, data) {
return value < data.get('max_price', 0)
}, 'Must be less than max_price.')
Parameters
fn(function) —@(value, data) -> boolmessage(string) — Optional custom error message.
Returns Validator
use()
validate.use(rule_class) -> Validator
Attaches a pre-defined Rule subclass (the class itself, not an
instance) directly. The class must extend Rule and accept only name
as its constructor argument.
import validate { Rule }
class Palindrome < Rule {
validate(value) {
if !is_string(value) return false
var s = value.lower()
return s == s.reverse()
}
error() { return 'The ${self.name} field must be a palindrome.' }
}
validate.use(Palindrome).string()
Parameters
rule_class(class) — A class that extendsRule.
Returns Validator
compress
import compress
The compress library provides in-memory and streaming compression and
decompression across several algorithms, plus archive-format support for
TAR and ZIP.
Compression methods
| Method | Submodule | Encode | Decode | Streaming | Notes |
|---|---|---|---|---|---|
| Deflate | compress.deflate | yes | yes | yes | RFC 1951 raw compress stream. |
| Zlib | compress.zlib | yes | yes | yes | RFC 1950 wrapper around Deflate; also re-exports Deflate’s GzipDecoder/GzipEncoder as DeflateDecoder/DeflateEncoder. |
| Gzip | compress.gzip | yes | yes | yes | RFC 1952 wrapper around Deflate. GzFile treats a .gz file like a regular file. |
| Zstandard | compress.zstd | yes | yes | yes | Pure-Rust implementation (zrip). |
| LZ4 | compress.lz4 | yes | yes | yes | Frame format, via lz4_flex. |
| Bzip2 | compress.bzip2 | yes | yes | yes | Pure-Rust implementation (libbz2-rs-sys). BzFile treats a .bz2 file like a regular file. Only decodes the first stream in a bzip2 “multistream” (see the submodule’s own docs). |
| Brotli | compress.brotli | yes | yes | decode only | Pure-Rust implementation. The streaming encoder buffers all input and compresses once in finish() rather than incrementally: see BrotliEncoder’s own docs. |
Every streaming submodule follows the same shape: a compress()/
decompress() one-shot pair for when the whole buffer is already in
memory, plus an Encoder/Decoder class pair
(write()/flush()/finish()/reset() and
read()/read_exact()/read_all()/read_as_string() respectively)
for when the input arrives incrementally or the output needs to be
consumed as it’s produced. total_in()/
total_out()/available()/finished() are available on both sides of
every streaming class for introspection.
compress.checksum provides CRC-32 and Adler-32, independent of any
particular compression method: used internally by zip to validate
extracted entries, but also usable standalone.
Archive formats
| Format | Submodule | Read | Write | Compression methods |
|---|---|---|---|---|
| TAR | compress.tar | yes | yes | none, gzip, bzip2 (COMPRESS_NONE/COMPRESS_GZIP/COMPRESS_BZIP) |
| ZIP | compress.zip | yes | yes | stored, deflate, bzip2 (ZIP_STORED/ZIP_DEFLATE/ZIP_BZIP2); zip64 supported for archives/files exceeding the classic 4 GiB limit |
Both tar and zip expose a low-level class (tar.Tar,
zip.ZipArchive) for building an archive entry by entry or inspecting
one in detail, and a pair of one-shot free functions
(compress()/extract()) for the common case of “archive this whole
file or directory” / “extract this whole archive”. zip.extract()
refuses to write outside the destination directory even if the archive
contains ../-style path-traversal entry names (a “Zip Slip” attack); a
ZIP entry that fails to decode cleanly (an unsupported or encrypted
entry, or one whose CRC-32 doesn’t match what the archive claims) is
reported per-entry via ZipItem.error rather than silently skipped or
allowed to abort the whole read.
TAR does not currently support the LZMA/xz compression method some
.tar.xz archives use, nor Brotli (no established TAR convention exists
for it). ZIP does not currently support LZMA (method 14), PPMd (method
98), or any of the ZIP encryption schemes (traditional PKWARE or AES):
an encrypted entry is reported via ZipItem.error rather than
attempted.
The compress API
Every public name in compress, wherever it is declared. Each links to
the page that documents it.
| Name | Kind | Summary |
|---|---|---|
compress.BEST_COMPRESSION | constant | Best compression level. |
compress.BEST_SPEED | constant | Best speed compression. |
compress.DEFAULT_COMPRESSION | constant | Default compression level. |
compress.DEFAULT_MEMORY_LEVEL | constant | Default memory level |
compress.DEFAULT_STRATEGY | constant | Default compression strategy. |
compress.FILTERED | constant | Filtered compression strategy. |
compress.FIXED | constant | Fixed compression strategy. |
compress.HUFFMAN_ONLY | constant | huffman only compression strategy |
compress.MAX_WBITS | constant | Maximum windows bit. |
compress.NO_COMPRESSION | constant | No compression level. |
compress.RLE | constant | Rle compression strategy. |
compress.ZlibDecoder | class | Streaming zlib decompressor implemention. |
compress.ZlibEncoder | class | A streaming Deflate encoder. |
compress.brotli.BrotliDecoder | class | Streaming Brotli decompressor implementation. |
compress.brotli.BrotliEncoder | class | A streaming Brotli encoder. |
compress.brotli.compress | function | Compress data using Brotli. |
compress.brotli.decompress | function | Decompress Brotli-compressed data. |
compress.bzip2.BzFile | class | The BzFile class implements a Bzip2 based I/O system that allows you to treat Bzip2 streams (bytes) as if… |
compress.bzip2.Bzip2Decoder | class | Streaming bzip2 decompressor implementation. |
compress.bzip2.Bzip2Encoder | class | A streaming Bzip2 encoder. |
compress.bzip2.compress | function | Compress data using the default options for Bzip2. |
compress.bzip2.decompress | function | Decompress Bzip2-compressed data. |
compress.checksum.adler32 | function | Updates a running Adler-32 checksum with the bytes buf[0,len-1] and return the updated checksum. |
compress.checksum.crc32 | function | Update a running CRC-32 checksum with the bytes buf[0,len-1] and return the updated CRC-32 checksum. |
compress.compress | function | Compress compresses as much data as possible, and stops when the input buffer becomes empty or the output… |
compress.decompress | function | Decompress decompresses as much data as possible, and stops when the input buffer becomes empty or the output… |
compress.deflate.DeflateDecoder | class | Streaming deflate decompressor implemention. |
compress.deflate.DeflateEncoder | class | A streaming Deflate encoder. |
compress.deflate.compress | function | Compress data using the default options for Deflate. |
compress.deflate.decompress | function | Decompress a deflated data using default options. |
compress.gzip.GzFile | class | The GzFile class implements a GZip based I/O system that allows you use treat Gzip streams (bytes) as if they… |
compress.gzip.GzipDecoder | class | Streaming gzip decompressor implemention. |
compress.gzip.GzipEncoder | class | A streaming GZip encoder. |
compress.gzip.compress | function | Compress data using the default options for GZip. |
compress.gzip.decompress | function | Decompress a GZipped data using default options. |
compress.lz4.Lz4Decoder | class | Streaming reader for decompressing the LZ4 frame format. |
compress.lz4.Lz4Encoder | class | Streaming lz4 compressor implemention. |
compress.lz4.compress | function | Compress data using the Lz4 block format. |
compress.lz4.decompress | function | Decompress a Lz4 compressed data. |
compress.tar.COMPRESS_AUTO | constant | Automatically select and detect compression type (Default). |
compress.tar.COMPRESS_BZIP | constant | Create and read archives with the BZip2 compression method. |
compress.tar.COMPRESS_GZIP | constant | Create and read archives with the GZip compression method. |
compress.tar.COMPRESS_NONE | constant | Create and read archives without any compression. |
compress.tar.Tar | class | |
compress.tar.TarCorruptedError | class | Error thrown when a TAR archive is corrupted. |
compress.tar.TarIOError | class | Error thrown when an I/O error occurs. |
compress.tar.TarIllegalCompressionError | class | Error thrown when an illegal compression type is used. |
compress.tar.compress | function | Create a new TAR ball from the file or directory in the given path and saves it to the destination path or… |
compress.tar.extract | function | Extracts a TAR file to the given destination or to the same directory as the source file with the same name… |
compress.zip.ZIP_BZIP2 | constant | Compression method that indicates Bzip2 compression |
compress.zip.ZIP_DEFLATE | constant | Compression method that indicates zlib Deflate compression |
compress.zip.ZIP_EXT | constant | The default zip file extension |
compress.zip.ZIP_FILE_COUNT_LIMIT | constant | The maximum number of files in a zip archive when zip64 is not used |
compress.zip.ZIP_FILE_MAX | constant | |
compress.zip.ZIP_MAX | constant | The maximum size of a zip archive when zip64 is not used |
compress.zip.ZIP_STORED | constant | Compression method that indicates no compression |
compress.zip.ZipArchive | class | ZipArchive provides a class for zip archive creation, manipulation and extraction. |
compress.zip.ZipFile | class | ZipFile represents an instance of zip file. |
compress.zip.ZipItem | class | ZipItem represents a single file or directory in a zip archive. |
compress.zip.compress | function | Compresses the given path (file or directory) into the destination zip archive. |
compress.zip.extract | function | Extracts the zip archive at the file path to the given destination directory. |
compress.zstd.ZstdDecoder | class | Streaming zstd decompressor implemention. |
compress.zstd.ZstdEncoder | class | Streaming zstd compressor implemention. |
compress.zstd.compress | function | Compress data using the default options for Zstd. |
compress.zstd.decompress | function | Decompress a Zstd compressed data. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
compress.brotli | compress.brotli.* | The Brotli submodule for the compress module. |
compress.bzip2 | compress.bzip2.* | The Bzip2 submodule for the compress module. |
compress.checksum | compress.checksum.* | This is the checksum submodule for the compress module. |
compress.deflate | compress.deflate.* | This is the Deflate submodule for the compress module. |
compress.gzip | compress.gzip.* | The is the GZip submodule for the compress module. |
compress.lz4 | compress.lz4.* | This is the Lz4 submodule for the compress module. |
compress.tar | compress.tar.* | This module adds support for creating and extracting TAR archives. |
compress.zip | compress.zip.* | The zip module contains classes and functions to make working with zip archives easy. |
compress.zlib | compress.* | This is the Zlib submodule for the compress module. |
compress.zstd | compress.zstd.* | This is the Zstd submodule for the compress module. |
1995-2017 Jean-loup Gailly and Mark Adler
compress.brotli
import compress
compressexposes this ascompress.brotli, soimport compressis enough and the names are called ascompress.brotli.*.import compress.brotlireaches the same definitions directly.
The Brotli submodule for the compress module.
Functions
compress()
compress.brotli.compress(data, quality, window) -> bytes
Compress data using Brotli.
Parameters
data(bytes|string)quality(?number) — Compression quality (0..=11, higher is slower but smaller): Default 11.window(?number) — Base-2 logarithm of the sliding window size (10..=24): Default 22.
Returns bytes
decompress()
compress.brotli.decompress(data) -> bytes
Decompress Brotli-compressed data.
Parameters
data(bytes|string)
Returns bytes
Classes
BrotliEncoder
class compress.brotli.BrotliEncoder
A streaming Brotli encoder.
Example:
%> import compress.brotli
%> var encoder = brotli.BrotliEncoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(1b 0a 00 f8 25 04 92 68 65 6c ...)
Note: Unlike the other streaming encoders in this module (Gzip, Bzip2, Zstd, Lz4), this one does not compress incrementally as
write()is called: it buffers everything you give it and does the real work once, infinish().available()therefore always reads0beforefinish()is called: there’s nothing waiting yet. This follows from the underlying encoder only exposing a “compress this all at once into a sink” interface, not a lower-level incremental one.
Constructor
compress.brotli.BrotliEncoder(quality, window)
Creates a new streaming Brotli encoder.
Parameters
quality(?number) — Compression quality (0..=11): Default 11.window(?number) — Base-2 logarithm of the sliding window size (10..=24): Default 22.
BrotliEncoder.finish()
compress.brotli.BrotliEncoder.finish() -> bytes
Compresses everything written so far and returns the resulting byte stream.
Returns bytes
Note: Once finish is called, the encoder can no longer be reused.
BrotliEncoder.close()
compress.brotli.BrotliEncoder.close() -> bytes
Same as finish() for file API compartibility.
Returns bytes
Note: Once finish is called, the encoder can no longer be reused.
BrotliEncoder.reset()
compress.brotli.BrotliEncoder.reset() -> bytes
Finishes the current stream and installs a fresh one for the next frame.
Returns the previous byte stream containing the completed frame.
Returns bytes
BrotliEncoder.write()
compress.brotli.BrotliEncoder.write(data) -> number
Buffers data to be compressed once finish() or reset() is called,
returning how many bytes were accepted.
Parameters
data(bytes|string)
Returns number
BrotliEncoder.available()
compress.brotli.BrotliEncoder.available() -> number
Returns the number of compressed bytes currently waiting in the
encoder’s output buffer. Always 0 before finish(): see the
class-level note on why this encoder isn’t incremental.
Returns number
BrotliEncoder.finished()
compress.brotli.BrotliEncoder.finished() -> bool
Returns whether finish() has completed the stream.
Returns bool
BrotliEncoder.total_in()
compress.brotli.BrotliEncoder.total_in() -> number
Number of uncompressed bytes consumed by the encoder so far (only
updated once finish()/reset() actually compresses them: see the
class-level note).
Returns number
BrotliEncoder.total_out()
compress.brotli.BrotliEncoder.total_out() -> number
Number of compressed bytes generated by the encoder so far.
Returns number
BrotliDecoder
class compress.brotli.BrotliDecoder
Streaming Brotli decompressor implementation.
Wraps a byte stream of compressed data and yields decompressed bytes.
Example:
%> import compress.brotli
%> var data = brotli.compress('hello world')
%> var decoder = brotli.BrotliDecoder(data)
%> decoder.read_as_string()
hello world
Constructor
compress.brotli.BrotliDecoder(source)
Creates a Brotli decoder
Parameters
source(bytes)
BrotliDecoder.reset()
compress.brotli.BrotliDecoder.reset(new_source)
Installs a new data source, discarding whatever remained of the previous one.
Parameters
new_source(bytes)
BrotliDecoder.close()
compress.brotli.BrotliDecoder.close()
Closes the decoder by resetting it into an empty stream.
BrotliDecoder.read()
compress.brotli.BrotliDecoder.read(length) -> bytes
Reads some bytes up to the amount of bytes specified by length from the current source. Returns an empty byte stream when there is no more data to read.
Parameters
length(number)
Returns bytes
BrotliDecoder.read_exact()
compress.brotli.BrotliDecoder.read_exact(length) -> bytes
Reads the exact number of bytes from the buffer. This method will raise
an error if it encounters an unexpected EOF (end of file) or there are
insufficient data to read to complete the required number of bytes.
Parameters
length(number)
Returns bytes
BrotliDecoder.read_all()
compress.brotli.BrotliDecoder.read_all() -> bytes
Reads all remaining bytes until EOF is encountered in the source.
Returns bytes
BrotliDecoder.read_as_string()
compress.brotli.BrotliDecoder.read_as_string() -> string
Reads all remaining bytes until EOF is encountered in the source and returns the data read as a string instead of a byte stream.
Returns string
BrotliDecoder.available()
compress.brotli.BrotliDecoder.available() -> number
Returns the number of compressed bytes currently waiting in the
decoder’s output buffer. Always 0: this decoder decodes on demand
rather than buffering ahead.
Returns number
BrotliDecoder.finished()
compress.brotli.BrotliDecoder.finished() -> bool
Returns whether the decoder has reached the end of the stream.
Returns bool
BrotliDecoder.total_in()
compress.brotli.BrotliDecoder.total_in() -> number
Number of compressed bytes in the decoder’s source.
Returns number
BrotliDecoder.total_out()
compress.brotli.BrotliDecoder.total_out() -> number
Number of uncompressed bytes generated by the decoder so far.
Returns number
2026, Richard Ore and Zuri contributors
compress.bzip2
import compress
compressexposes this ascompress.bzip2, soimport compressis enough and the names are called ascompress.bzip2.*.import compress.bzip2reaches the same definitions directly.
The Bzip2 submodule for the compress module.
Functions
compress()
compress.bzip2.compress(data, level) -> bytes
Compress data using the default options for Bzip2.
Parameters
data(bytes|string)level(?number) — Compression level (1..=9): Default 9.
Returns bytes
decompress()
compress.bzip2.decompress(data) -> bytes
Decompress Bzip2-compressed data.
Parameters
data(bytes|string)
Returns bytes
Note: This only decodes the first bzip2 stream in
data. Some tools (pbzip2, Wikipedia data dumps, …) concatenate several independent bzip2 streams back to back (“multistreams”); useBzip2Decoderin a loop withreset()to decode all of them.
Classes
Bzip2Encoder
class compress.bzip2.Bzip2Encoder
A streaming Bzip2 encoder.
Example:
%> import compress.bzip2
%> var encoder = bzip2.Bzip2Encoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(42 5a 68 39 31 41 59 26 53 59 ...)
Constructor
compress.bzip2.Bzip2Encoder(level, work_factor)
Creates a new streaming Bzip2 encoder.
Parameters
level(?number) — Compression level (1..=9): Default 9.work_factor(?number) — Controls when the compressor falls back from its standard sorting algorithm to a slower but more robust one on highly repetitive input. 0..=250: 0 (the default) means “use bzip2’s own default of 30”.
Bzip2Encoder.finish()
compress.bzip2.Bzip2Encoder.finish() -> bytes
Flushes remaining data, writes the stream footer, and returns the inner byte stream.
Returns bytes
Note: Once finish is called, the encoder can no longer be reused.
Bzip2Encoder.close()
compress.bzip2.Bzip2Encoder.close() -> bytes
Same as finish() for file API compartibility.
Returns bytes
Note: Once finish is called, the encoder can no longer be reused.
Bzip2Encoder.reset()
compress.bzip2.Bzip2Encoder.reset() -> bytes
Finishes the current stream and installs a fresh one for the next frame.
Returns the previous byte stream containing the completed frame.
Returns bytes
Bzip2Encoder.write()
compress.bzip2.Bzip2Encoder.write(data) -> number
Writes data into this encoder’s byte stream, returning how many bytes were written.
Parameters
data(bytes|string)
Returns number
Bzip2Encoder.flush()
compress.bzip2.Bzip2Encoder.flush() -> bytes
Flushes this output stream and returns the compressed data produced so far, without terminating the bzip2 stream.
Returns bytes
Bzip2Encoder.available()
compress.bzip2.Bzip2Encoder.available() -> number
Returns the number of compressed bytes currently waiting in the encoder’s output buffer.
Returns number
Bzip2Encoder.finished()
compress.bzip2.Bzip2Encoder.finished() -> bool
Returns whether finish() has completed the stream.
Returns bool
Bzip2Encoder.total_in()
compress.bzip2.Bzip2Encoder.total_in() -> number
Number of uncompressed bytes consumed by the encoder.
Returns number
Bzip2Encoder.total_out()
compress.bzip2.Bzip2Encoder.total_out() -> number
Number of compressed bytes generated by the encoder.
Returns number
Bzip2Decoder
class compress.bzip2.Bzip2Decoder
Streaming bzip2 decompressor implementation.
Wraps a byte stream of compressed data and yields decompressed bytes.
Example:
%> import compress.bzip2
%> var data = bzip2.compress('hello world')
%> var decoder = bzip2.Bzip2Decoder(data)
%> decoder.read_as_string()
hello world
Note: only decodes a single bzip2 stream: see
decompress()’s note about “multistreams”.
Constructor
compress.bzip2.Bzip2Decoder(source)
Creates a Bzip2 decoder
Parameters
source(bytes)
Bzip2Decoder.reset()
compress.bzip2.Bzip2Decoder.reset(new_source)
Installs a new data source, discarding whatever remained of the previous one.
Parameters
new_source(bytes)
Bzip2Decoder.close()
compress.bzip2.Bzip2Decoder.close()
Closes the decoder by resetting it into an empty stream.
Bzip2Decoder.read()
compress.bzip2.Bzip2Decoder.read(length) -> bytes
Reads some bytes up to the amount of bytes specified by length from the current source. Returns an empty byte stream when there is no more data to read.
Parameters
length(number)
Returns bytes
Bzip2Decoder.read_exact()
compress.bzip2.Bzip2Decoder.read_exact(length) -> bytes
Reads the exact number of bytes from the buffer. This method will raise
an error if it encounters an unexpected EOF (end of file) or there are
insufficient data to read to complete the required number of bytes.
Parameters
length(number)
Returns bytes
Bzip2Decoder.read_all()
compress.bzip2.Bzip2Decoder.read_all() -> bytes
Reads all remaining bytes until EOF is encountered in the source.
Returns bytes
Bzip2Decoder.read_as_string()
compress.bzip2.Bzip2Decoder.read_as_string() -> string
Reads all remaining bytes until EOF is encountered in the source and returns the data read as a string instead of a byte stream.
Returns string
Bzip2Decoder.available()
compress.bzip2.Bzip2Decoder.available() -> number
Returns the number of compressed bytes still unread in the decoder’s source.
Returns number
Bzip2Decoder.finished()
compress.bzip2.Bzip2Decoder.finished() -> bool
Returns whether the decoder has reached the end of the stream.
Returns bool
Bzip2Decoder.total_in()
compress.bzip2.Bzip2Decoder.total_in() -> number
Number of compressed bytes consumed by the decoder.
Returns number
Bzip2Decoder.total_out()
compress.bzip2.Bzip2Decoder.total_out() -> number
Number of uncompressed bytes generated by the decoder.
Returns number
BzFile
class compress.bzip2.BzFile
The BzFile class implements a Bzip2 based I/O system that allows you to treat Bzip2 streams (bytes) as if they were a file.
The class implements the essentials of a file except those that ties it to the operating system filesystem such as symbolic links, chmod and set time.
See the chapter on files in The Zuri Programming Language for more information.
Constructor
compress.bzip2.BzFile(path: string, mode: ?string, _inner)
Returns a new BzFile object bounded to a physical file at the given path and opened in the given mode. See [[file]] for a description of the supported file modes.
Parameters
path(string)mode(?string)
Returns BzFile
BzFile.exists()
compress.bzip2.BzFile.exists() -> bool
Returns true if the underlying file bounded to BzFile actually exists
or false otherwise.
Returns bool
BzFile.close()
compress.bzip2.BzFile.close()
Closes the stream to an opened BzFile. You’ll rarely ever need to call this method yourself in most use cases.
BzFile.flush()
compress.bzip2.BzFile.flush() -> bytes
Flushes the remaning data into the BzFile underlying file and returns the byte stream returned.
Returns bytes
BzFile.open()
compress.bzip2.BzFile.open() -> bool
Opens the stream to a BzFile for the operation originally specified on the BzFile object during creation.
You may need to call this method after a call to read() if the length
isn’t specified or write() if you wish to read or write again as the
BzFile will already be closed.
Returns bool
BzFile.read()
compress.bzip2.BzFile.read(length: ?number) -> bytes
Reads the content of an opened BzFile up to the specified length and returns it as string or bytes if the BzFile was opened in the binary mode. If the length is not specified, the BzFile will be read to the end.
This method requires that the BzFile be opened in the read mode (default mode) or a mode that supports reading. If you aren’t reading the full length of the BzFile, you’ll need to call the close() method to free the BzFile for further reading, otherwise, the close() method will be automatically called for you.
Parameters
length(number) — Default = -1
Returns bytes
Raises Error
BzFile.gets()
compress.bzip2.BzFile.gets(length: ?number) -> bytes
Same as read(), but doesn’t close the BzFile automatically.
Parameters
length(?number) — Default = -1, meaning read to the end.
Returns bytes
Raises Error
BzFile.write()
compress.bzip2.BzFile.write(data: bytes|string) -> number
Writes a string or bytes to an opened BzFile at the current insertion point. When the BzFile is opened with the a mode enabled, write will always start from the end of the BzFile.
If the seek() method has been previously called, write will begin from the seeked position, otherwise it will start at the beginning of the BzFile.
Parameters
- `` (
bytes|string)
Returns number
BzFile.puts()
compress.bzip2.BzFile.puts(data: bytes|string) -> number
Same as write(), but doesn’t open or close the BzFile automatically.
Parameters
- `` (
bytes|string)
Returns number
BzFile.number()
compress.bzip2.BzFile.number() -> number
Returns the integer file descriptor number that is used by the underlying implementation to request I/O operations from the operating system. This can be very useful for low-level interfaces that uses or act as BzFile descriptors.
Returns number
BzFile.is_tty()
compress.bzip2.BzFile.is_tty() -> bool
Returns true if the underlying file of BzFile is a TTY device or
false otherwise.
Returns bool
BzFile.is_open()
compress.bzip2.BzFile.is_open() -> bool
Returns true if the BzFile is open for reading or writing and false
otherwise.
Returns bool
BzFile.is_closed()
compress.bzip2.BzFile.is_closed()
Returns true if the BzFile is closed for reading or writing and
false otherwise.
BzFile.symlink()
compress.bzip2.BzFile.symlink(path: string) -> bool
See [[file.symlink]]
Returns bool
BzFile.stats()
compress.bzip2.BzFile.stats() -> dict
Returns the statistics or details of the BzFile.
See the working with files documentation for more information about the
stats() method.
Returns dict
BzFile.delete()
compress.bzip2.BzFile.delete() -> bool
Deletes the underlying file pointed to by BzFile
Any further attempt to perform most operations on the BzFile after
calling delete() will raise an error.
Returns bool
BzFile.rename()
compress.bzip2.BzFile.rename(new_name) -> bool
Renames the underlying file pointed to by BzFile to the new name and
returns true if it succeeds or false otherwise.
Returns bool
BzFile.copy()
compress.bzip2.BzFile.copy() -> [[compress.bzip2.BzFile]]
Returns a new BzFile reading (or writing) the same underlying path independently of this one, with its own decoder/encoder state and its own position.
Returns [[compress.bzip2.BzFile]]
BzFile.path()
compress.bzip2.BzFile.path() -> string
Returns the physical path of the file pointed to by BzFile.
Returns string
BzFile.abs_path()
compress.bzip2.BzFile.abs_path() -> string
Returns the absolute path of the physical file pointed to by BzFile.
Returns string
BzFile.truncate()
compress.bzip2.BzFile.truncate(length: ?int) -> bool
Truncates the entire BzFile if length is not given or truncates the BzFile such that only length number of bytes is left in it.
Returns bool
BzFile.chmod()
compress.bzip2.BzFile.chmod(number: int) -> bool
Updates the permission for the file bounded by BzFile.
Returns bool
BzFile.set_times()
compress.bzip2.BzFile.set_times(atime: int, mtime: int) -> bool
Sets the last access time and last modified time of the BzFile.
Returns bool
BzFile.seek()
compress.bzip2.BzFile.seek(position: int, seek_type: int) -> bool
Sets the position of a BzFile reader in the decompressed byte stream
(not the raw, still-compressed bytes on disk, which have no useful
correspondence to a decompressed offset). The seek_type argument must
be one of [[io.SEEK_SET]], [[io.SEEK_CUR]] or [[io.SEEK_END]].
Seeking forward simply discards decompressed bytes until the target position, since a streaming decoder has no way to skip ahead without producing them. Seeking backward (or SEEK_END, which has to fully decode the stream once to learn its length) re-opens the underlying file and restarts decompression from the beginning, discarding up to the target position: an inherently expensive operation for a compressed stream, same as with any other streaming (de)compressor.
Not supported on a BzFile opened for writing.
Returns bool
BzFile.tell()
compress.bzip2.BzFile.tell() -> number
Returns the current position of the reader in the decompressed byte
stream (bytes already consumed via read()/gets()/seek()), or of
the writer in the uncompressed input already fed to write()/puts()
for a BzFile opened for writing.
Returns number
BzFile.mode()
compress.bzip2.BzFile.mode() -> string
Returns the mode in which the current BzFile was opened.
Returns string
BzFile.name()
compress.bzip2.BzFile.name() -> string
Returns the name of the file pointed to by BzFile.
Returns string
2026, Richard Ore and Zuri contributors
compress.checksum
import compress
compressexposes this ascompress.checksum, soimport compressis enough and the names are called ascompress.checksum.*.import compress.checksumreaches the same definitions directly.
This is the checksum submodule for the compress module.
Functions
adler32()
compress.checksum.adler32(data, initial) -> number
Updates a running Adler-32 checksum with the bytes buf[0,len-1] and return the updated checksum.
Parameters
data(bytes|string)initial(?number) — Default1: the algorithm’s own defined identity/seed value (RFC 1950). Pass the previously returned checksum here to continue an incremental computation across multiple calls.
Returns number
Note: An Adler-32 checksum is almost as reliable as a CRC-32 but can be computed much faster.
crc32()
compress.checksum.crc32(data, initial) -> number
Update a running CRC-32 checksum with the bytes buf[0,len-1] and return the updated CRC-32 checksum.
Parameters
data(bytes|string)initial(?number) — Default0: the algorithm’s own defined identity/seed value. Pass the previously returned checksum here to continue an incremental computation across multiple calls.
Returns number
2021, Richard Ore and Zuri contributors
compress.deflate
import compress
compressexposes this ascompress.deflate, soimport compressis enough and the names are called ascompress.deflate.*.import compress.deflatereaches the same definitions directly.
This is the Deflate submodule for the compress module.
Functions
compress()
compress.deflate.compress(data) -> bytes
Compress data using the default options for Deflate.
Parameters
data(bytes|string)
Returns bytes
decompress()
compress.deflate.decompress(data) -> bytes
Decompress a deflated data using default options.
Parameters
data(bytes|string)
Returns bytes
Classes
DeflateEncoder
class compress.deflate.DeflateEncoder < GzipEncoder
A streaming Deflate encoder.
Example:
%> import compress.deflate
%> var encoder = deflate.DeflateEncoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00)
Constructor
compress.deflate.DeflateEncoder(level)
Creates a new streaming Deflate encoder at the given level (0..=9).
Parameters
level(?number) — Default 1
DeflateDecoder
class compress.deflate.DeflateDecoder < GzipDecoder
Streaming deflate decompressor implemention. This decoder reads standard deflate blocks and frames produced at any deflate compression level.
Wraps a byte stream of compressed data and yields decompressed bytes.
Example:
%> import compress.deflate
%> var data = deflate.compress('hello world')
%> data
(cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00)
%>
%> var decoder = deflate.DeflateDecoder(data)
%> decoder.read_as_string()
hello world
This decoder is level-independent and supports standard deflate frames produced by all deflate compression levels.
Constructor
compress.deflate.DeflateDecoder(source)
Creates a Gzip decoder
Parameters
source(bytes)
2021, Richard Ore and Zuri contributors
compress.gzip
import compress
compressexposes this ascompress.gzip, soimport compressis enough and the names are called ascompress.gzip.*.import compress.gzipreaches the same definitions directly.
The is the GZip submodule for the compress module.
Functions
compress()
compress.gzip.compress(data) -> bytes
Compress data using the default options for GZip.
Parameters
data(bytes|string)
Returns bytes
decompress()
compress.gzip.decompress(data) -> bytes
Decompress a GZipped data using default options.
Parameters
data(bytes|string)
Returns bytes
Classes
GzipEncoder
class compress.gzip.GzipEncoder
A streaming GZip encoder.
Example:
%> import compress.gzip
%> var encoder = gzip.GzipEncoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(1f 8b 08 00 00 00 00 00 04 03 cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00 85 11 4a 0d 0b 00 00 00)
Constructor
compress.gzip.GzipEncoder(level)
Creates a new streaming GZip encoder at the given level (0..=9).
Parameters
level(?number) — Default 1
GzipEncoder.finish()
compress.gzip.GzipEncoder.finish() -> bytes
Flushes remaining data, writes the content checksum, and returns the inner byte stream.
Returns bytes
Note: Once finish is called, the encoder can no longer be reused.
GzipEncoder.close()
compress.gzip.GzipEncoder.close() -> bytes
Same as finish() for file API compartibility.
Returns bytes
Note: Once finish is called, the encoder can no longer be reused.
GzipEncoder.reset()
compress.gzip.GzipEncoder.reset() -> bytes
Finishes the current frame and installs byte stream for the next one.
Returns the previous byte stream containing the completed frame. All internal buffers (hash tables, workspace, block scratch) stay allocated and are reused for the next frame.
Returns bytes
GzipEncoder.write()
compress.gzip.GzipEncoder.write(data) -> number
Writes data into this encoder’s byte stream, returning how many bytes were written.
Parameters
data(bytes|string)
Returns number
GzipEncoder.flush()
compress.gzip.GzipEncoder.flush() -> bytes
Flushes this output stream and returns the remaining compressed data in the stream, ensuring that all intermediately buffered contents reach their destination.
Returns bytes
GzipEncoder.available()
compress.gzip.GzipEncoder.available() -> number
Returns the number of compressed bytes currently waiting in the encoder’s output buffer.
Returns number
GzipEncoder.finished()
compress.gzip.GzipEncoder.finished() -> bool
Returns whether finish() has completed the stream.
Returns bool
GzipEncoder.total_in()
compress.gzip.GzipEncoder.total_in() -> number
Number of uncompressed bytes consumed by the encoder.
Returns number
GzipEncoder.total_out()
compress.gzip.GzipEncoder.total_out() -> number
Number of compressed bytes generated by the encoder.
Returns number
GzipDecoder
class compress.gzip.GzipDecoder
Streaming gzip decompressor implemention. This decoder reads standard gzip blocks and frames produced at any gzip compression level.
Wraps a byte stream of compressed data and yields decompressed bytes.
Example:
%> import compress.gzip
%> var data = gzip.compress('hello world')
%> data
(1f 8b 08 00 00 00 00 00 00 03 cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00 85 11 4a 0d 0b 00 00 00)
%>
%> var decoder = gzip.GzipDecoder(data)
%> decoder.read_as_string()
hello world
This decoder is level-independent and supports standard gzip frames produced by all gzip compression levels.
Constructor
compress.gzip.GzipDecoder(source)
Creates a Gzip decoder
Parameters
source(bytes)
GzipDecoder.reset()
compress.gzip.GzipDecoder.reset(new_source)
Installs a new data source for the next frame, keeping all internal buffers allocated.
Parameters
new_source(bytes)
GzipDecoder.close()
compress.gzip.GzipDecoder.close()
Closes the decoder by resetting it into an empty stream.
GzipDecoder.read()
compress.gzip.GzipDecoder.read(length) -> bytes
Reads some bytes up to the amount of bytes specified by length from the current source. Returns an empty byte stream when there is no more data to read.
This function does not block or wait waiting for data, but reads as much data as is available to read when it runs.
Parameters
length(number)
Returns bytes
GzipDecoder.read_exact()
compress.gzip.GzipDecoder.read_exact(length) -> bytes
Reads the exact number of bytes from the buffer. This method will raise
an error if it encounters an unexpected EOF (end of file) or there are
insufficient data to read to complete the required number of bytes.
Parameters
length(number)
Returns bytes
GzipDecoder.read_all()
compress.gzip.GzipDecoder.read_all() -> bytes
Reads all remaining bytes until EOF is encountered in the source.
Returns bytes
GzipDecoder.read_as_string()
compress.gzip.GzipDecoder.read_as_string() -> string
Reads all remaining bytes until EOF is encountered in the source and returns the data read as a string instead of a byte stream.
Returns string
GzipDecoder.available()
compress.gzip.GzipDecoder.available() -> number
Returns the number of compressed bytes currently waiting in the decoder’s output buffer.
Returns number
GzipDecoder.finished()
compress.gzip.GzipDecoder.finished() -> bool
Returns whether finish() has completed the stream.
Returns bool
GzipDecoder.total_in()
compress.gzip.GzipDecoder.total_in() -> number
Number of compressed bytes consumed by the decoder.
Returns number
GzipDecoder.total_out()
compress.gzip.GzipDecoder.total_out() -> number
Number of uncompressed bytes generated by the decoder.
Returns number
GzFile
class compress.gzip.GzFile
The GzFile class implements a GZip based I/O system that allows you use treat Gzip streams (bytes) as if they were a file.
The class implements the essentials of a file except those that ties it to the operating system filesystem such as symbolic links, chmod and set time.
See the chapter on files in The Zuri Programming Language for more information.
Constructor
compress.gzip.GzFile(path: string, mode: ?string, _inner)
Returns a new GzFile object bounded to a physical file at the given path and opened in the given mode. See [[file]] for a description of the supported file modes.
Parameters
path(string)mode(?string)
Returns GzFile
GzFile.exists()
compress.gzip.GzFile.exists() -> bool
Returns true if the underlying file bounded to GzFile actually exists
or false otherwise.
Returns bool
GzFile.close()
compress.gzip.GzFile.close()
Closes the stream to an opened GzFile. You’ll rarely ever need to call this method yourself in most use cases.
GzFile.flush()
compress.gzip.GzFile.flush() -> bytes
Flushes the remaning data into the GzFile underlying file and returns the byte stream returned.
Returns bytes
GzFile.open()
compress.gzip.GzFile.open() -> bool
Opens the stream to a GzFile for the operation originally specified on the GzFile object during creation.
You may need to call this method after a call to read() if the length
isn’t specified or write() if you wish to read or write again as the
GzFile will already be closed.
Returns bool
GzFile.read()
compress.gzip.GzFile.read(length: ?number) -> bytes
Reads the content of an opened GzFile up to the specified length and returns it as string or bytes if the GzFile was opened in the binary mode. If the length is not specified, the GzFile will be read to the end.
This method requires that the GzFile be opened in the read mode (default mode) or a mode that supports reading. If you aren’t reading the full length of the GzFile, you’ll need to call the close() method to free the GzFile for further reading, otherwise, the close() method will be automatically called for you.
Parameters
length(number) — Default = -1
Returns bytes
Raises Error
GzFile.gets()
compress.gzip.GzFile.gets(length: ?number) -> bytes
Same as read(), but doesn’t close the GzFile automatically.
Parameters
length(?number) — Default = -1, meaning read to the end.
Returns bytes
Raises Error
GzFile.write()
compress.gzip.GzFile.write(data: bytes|string) -> number
Writes a string or bytes to an opened GzFile at the current insertion point. When the GzFile is opened with the a mode enabled, write will always start from the end of the GzFile.
If the seek() method has been previously called, write will begin from the seeked position, otherwise it will start at the beginning of the GzFile.
Parameters
- `` (
bytes|string)
Returns number
GzFile.puts()
compress.gzip.GzFile.puts(data: bytes|string) -> number
Same as write(), but doesn’t open or close the GzFile automatically.
Parameters
- `` (
bytes|string)
Returns number
GzFile.number()
compress.gzip.GzFile.number() -> number
Returns the integer file descriptor number that is used by the underlying implementation to request I/O operations from the operating system. This can be very useful for low-level interfaces that uses or act as GzFile descriptors.
Returns number
GzFile.is_tty()
compress.gzip.GzFile.is_tty() -> bool
Returns true if the underlying file of GzFile is a TTY device or
false otherwise.
Returns bool
GzFile.is_open()
compress.gzip.GzFile.is_open() -> bool
Returns true if the GzFile is open for reading or writing and false
otherwise.
Returns bool
GzFile.is_closed()
compress.gzip.GzFile.is_closed()
Returns true if the GzFile is closed for reading or writing and
false otherwise.
GzFile.symlink()
compress.gzip.GzFile.symlink(path: string) -> bool
See [[file.symlink]]
Returns bool
GzFile.stats()
compress.gzip.GzFile.stats() -> dict
Returns the statistics or details of the GzFile.
See the working with files documentation for more information about the
stats() method.
Returns dict
GzFile.delete()
compress.gzip.GzFile.delete() -> bool
Deletes the underlying file pointed to by GzFile
Any further attempt to perform most operations on the GzFile after
calling delete() will raise an error.
Returns bool
GzFile.rename()
compress.gzip.GzFile.rename(new_name) -> bool
Renames the underlying file pointed to by GzFile to the new name and
returns true if it succeeds or false otherwise.
Returns bool
GzFile.copy()
compress.gzip.GzFile.copy() -> [[io.GzFile]]
Returns a new GzFile reading (or writing) the same underlying path independently of this one, with its own decoder/encoder state and its own position.
Returns [[io.GzFile]]
GzFile.path()
compress.gzip.GzFile.path() -> string
Returns the physical path of the file pointed to by GzFile.
Returns string
GzFile.abs_path()
compress.gzip.GzFile.abs_path() -> string
Returns the absolute path of the physical file pointed to by GzFile.
Returns string
GzFile.truncate()
compress.gzip.GzFile.truncate(length: ?int) -> bool
Truncates the entire GzFile if length is not given or truncates the GzFile such that only length number of bytes is left in it.
Returns bool
GzFile.chmod()
compress.gzip.GzFile.chmod(number: int) -> bool
Updates the permission for the file bounded by GzFile.
Returns bool
GzFile.set_times()
compress.gzip.GzFile.set_times(atime: int, mtime: int) -> bool
Sets the last access time and last modified time of the GzFile.
Returns bool
GzFile.seek()
compress.gzip.GzFile.seek(position: int, seek_type: int) -> bool
Sets the position of a GzFile reader in the decompressed byte stream
(not the raw, still-compressed bytes on disk, which have no useful
correspondence to a decompressed offset). The seek_type argument must
be one of [[io.SEEK_SET]], [[io.SEEK_CUR]] or [[io.SEEK_END]].
Seeking forward simply discards decompressed bytes until the target position, since a streaming decoder has no way to skip ahead without producing them. Seeking backward (or SEEK_END, which has to fully decode the stream once to learn its length) re-opens the underlying file and restarts decompression from the beginning, discarding up to the target position: an inherently expensive operation for a compressed stream, same as with any other streaming (de)compressor.
Not supported on a GzFile opened for writing.
Returns bool
GzFile.tell()
compress.gzip.GzFile.tell() -> number
Returns the current position of the reader in the decompressed byte
stream (bytes already consumed via read()/gets()/seek()), or of
the writer in the uncompressed input already fed to write()/puts()
for a GzFile opened for writing.
Returns number
GzFile.mode()
compress.gzip.GzFile.mode() -> string
Returns the mode in which the current GzFile was opened.
Returns string
GzFile.name()
compress.gzip.GzFile.name() -> string
Returns the name of the file pointed to by GzFile.
Returns string
2021, Richard Ore and Zuri contributors
compress.lz4
import compress
compressexposes this ascompress.lz4, soimport compressis enough and the names are called ascompress.lz4.*.import compress.lz4reaches the same definitions directly.
This is the Lz4 submodule for the compress module.
This modules provides two ways to use lz4. The first way is through the
Lz4Decoder and Lz4Encoder classes, which implement a streaming
encoder and decoder with the lz4 frame format. Unless you have a
specific reason to the contrary, you should only use the Lz4 classes
which implement the lz4 frame format. Specifically, the lz4 frame format
permits streaming compression or decompression.
The second way is through the compress and decompress functions.
These functions provide access to the lz4 block format, and don’t
support a streaming interface directly. You should only use these types
if you know you specifically need the lz4 block format.
Functions
compress()
compress.lz4.compress(data) -> bytes
Compress data using the Lz4 block format. The uncompressed size will be prepended as a little endian unsigned integer 32.
Parameters
data(bytes|string)level(?number)
Returns bytes
Note: This is the block format, not the frame format:
Lz4Encoder/Lz4Decoderuse a genuinely different, mutually incompatible binary layout. Data compressed here can only be decompressed withdecompress(), never withLz4Decoder, and vice versa.
decompress()
compress.lz4.decompress(data) -> bytes
Decompress a Lz4 compressed data. The first 4 bytes are expected to be the uncompressed size in little endian.
Parameters
data(bytes|string)
Returns bytes
Note: This expects the block format
compress()produces, not the frame formatLz4Encoderproduces: passingLz4Encoderoutput here will fail (and vice versa forLz4Decodergivencompress()output). See the module docs.
Classes
Lz4Encoder
class compress.lz4.Lz4Encoder
Streaming lz4 compressor implemention. Data written to this encoder are compressed using the LZ4 frame format and are automatically buffered.
To ensure a well formed stream the encoder must be finalized by calling
the finish() method.
Example:
%> import compress.lz4
%> var encoder = lz4.Lz4Encoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(04 22 4d 18 60 40 82 0b 00 00 80 68 65 6c 6c 6f 20 77 6f 72 6c 64 00 00 00 00)
Note: This produces the LZ4 frame format: decode it with
Lz4Decoder, not the one-shotdecompress()(which expects a different, incompatible block format). See the module docs.
Constructor
compress.lz4.Lz4Encoder()
Creates a new streaming Lz4 encoder.
Lz4Encoder.finish()
compress.lz4.Lz4Encoder.finish() -> bytes
Flushes remaining data, writes the content checksum, and returns the inner byte stream.
Returns bytes
Note: Once finish is called, the encoder can no longer be reused.
Lz4Encoder.close()
compress.lz4.Lz4Encoder.close() -> bytes
Same as finish() for file API compartibility.
Returns bytes
Note: Once finish is called, the encoder can no longer be reused.
Lz4Encoder.write()
compress.lz4.Lz4Encoder.write(data) -> number
Writes data into this encoder’s byte stream, returning how many bytes were written.
Parameters
data(bytes|string)
Returns number
Lz4Encoder.write_all()
compress.lz4.Lz4Encoder.write_all(data)
Attempts to write an entire data into this encoder’s byte stream.
Parameters
data(bytes|string)
Lz4Encoder.flush()
compress.lz4.Lz4Encoder.flush()
Flushes this output stream, ensuring that all intermediately buffered contents reach their destination.
Lz4Decoder
class compress.lz4.Lz4Decoder
Streaming reader for decompressing the LZ4 frame format. Bytes read will be decompressed according to the LZ4 frame format.
Example:
%> import compress.lz4
%> var x = lz4.Lz4Encoder()
%> x.write('hello world')
11
%> var g = x.finish()
%> g
(04 22 4d 18 60 40 82 0b 00 00 80 68 65 6c 6c 6f 20 77 6f 72 6c 64 00 00 00 00)
%>
%> var f = lz4.Lz4Decoder(g)
%> f.read_as_string()
hello world
Note: This reads the LZ4 frame format produced by
Lz4Encoder: it cannot decode the one-shotcompress()’s block format (usedecompress()for that instead). See the module docs.
Constructor
compress.lz4.Lz4Decoder(source)
Creates a Lz4 decoder
Parameters
source(bytes)
Lz4Decoder.close()
compress.lz4.Lz4Decoder.close()
No op. Just for file API compartibility.
Lz4Decoder.read()
compress.lz4.Lz4Decoder.read(length) -> bytes
Reads some bytes up to the amount of bytes specified by length from the current source. Returns an empty byte stream when there is no more data to read.
This function does not block or wait waiting for data, but reads as much data as is available to read when it runs.
Parameters
length(number)
Returns bytes
Lz4Decoder.read_exact()
compress.lz4.Lz4Decoder.read_exact(length) -> bytes
Reads the exact number of bytes from the buffer. This method will raise
an error if it encounters an unexpected EOF (end of file) or there are
insufficient data to read to complete the required number of bytes.
Parameters
length(number)
Returns bytes
Lz4Decoder.read_all()
compress.lz4.Lz4Decoder.read_all() -> bytes
Reads all remaining bytes until EOF is encountered in the source.
Returns bytes
Lz4Decoder.read_as_string()
compress.lz4.Lz4Decoder.read_as_string() -> string
Reads all remaining bytes until EOF is encountered in the source and returns the data read as a string instead of a byte stream.
Returns string
2021, Richard Ore and Zuri contributors
compress.tar
import compress
compressexposes this ascompress.tar, soimport compressis enough and the names are called ascompress.tar.*.import compress.tarreaches the same definitions directly.
This module adds support for creating and extracting TAR archives.
Important
The library exports two helper function compress() and extract() by
that allows you to create and/or extract TAR archives.
This library supports the most popular extensions such as .tar.gz,
.tar, .gz, .tgz, .tar.bz2, .bz2, and .tbz: the three archive
shapes: uncompressed, gzip-compressed, and bzip2-compressed.
Extracting TAR archives
Use the helper function extract() for a quick way to extract TAR
archives.
import tar
tar.extract('/path/to/archive.tar.gz', '/destination')
The destination can be omitted in which case the archive will be
extracted into the same directory as the source with same name without
the last extension. e.g. for /path/to/file.tar.gz will extract to
/path/to/file.tar directory is the destination is not given.
See below for learn more about the extract() method
Creating a new TAR ball
To quickly create a new tarball, you can use the compress() helper
function in the library like this,
import tar
tar.compress('/path/to/file/or/directory', '/destination.tar.gz')
The compress function can be used to compress a single file or an entire
directory. Like the extract() function, you can choose to omit the
destination parameter in which case compress will save the file to the
current working directory with the same name as the file/directory with
the extension .tar.gz.
See below for learn more about the compress() method
Constants
COMPRESS_AUTO
compress.tar.COMPRESS_AUTO
Automatically select and detect compression type (Default).
COMPRESS_NONE
compress.tar.COMPRESS_NONE = 0
Create and read archives without any compression.
COMPRESS_GZIP
compress.tar.COMPRESS_GZIP = 1
Create and read archives with the GZip compression method.
COMPRESS_BZIP
compress.tar.COMPRESS_BZIP = 2
Create and read archives with the BZip2 compression method.
Functions
compress()
compress.tar.compress(path: string, destination: string)
Create a new TAR ball from the file or directory in the given path and saves it to the destination path or ${NAME_OF_FILE}.tar.gz in the current directory if the destination is not given.
Parameters
path(string) — the file or directory that will be compressed.destination(string) — the destination of the compressed TAR ball.
extract()
compress.tar.extract(file: string, destination: string) -> list
Extracts a TAR file to the given destination or to the same directory as the source file with the same name as the TAR file (without the last extension) if destination is not given.
Parameters
file(string) — the file to be extracteddestination(string) — the path to extract to (Optional).
Returns list — list of extracted items
Classes
TarCorruptedError
class compress.tar.TarCorruptedError < Error
Error thrown when a TAR archive is corrupted.
TarIOError
class compress.tar.TarIOError < Error
Error thrown when an I/O error occurs.
TarIllegalCompressionError
class compress.tar.TarIllegalCompressionError < Error
Error thrown when an illegal compression type is used.
Tar
class compress.tar.Tar
Tar.set_compression()
compress.tar.Tar.set_compression(level: ?int, type: ?int)
Set the compression level and type.
Parameters
level(int) — Compression level (0 to 9) - Default = 9type(int) — Type of compression to use (use COMPRESS_* constants)- Default = COMPRESS_AUTO
Raises TarIllegalCompressionError
Tar.open()
compress.tar.Tar.open(path: string)
Open an existing Tar file for reading.
Parameters
file(string)
Raises TarIOError
Raises TarIllegalCompressionError
Tar.contents()
compress.tar.Tar.contents() -> list[dict]
Read the contents of a Tar archive
This function lists the files stored in the Tar, and returns an indexed array of FileInfo objects
The Tar is closed afer reading the contents, because rewinding is not possible in bzip2 streams. Reopen the file with open() again if you want to do additional operations
Returns list[dict]
Tar.extract()
compress.tar.Tar.extract(outdir: string, strip: ?int|string, exclude: ?string, include: ?string) -> list[string]
Extract an existing Tar archive
The strip parameter allows you to strip a certain number of path components from the filenames found in the Tar file, similar to the –strip-components feature of GNU tar. This is triggered when an integer is passed as strip. Alternatively a fixed string prefix may be passed in strip. If the filename matches this prefix, the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
By default this will extract all files found in the Tar. You can restrict the output using the include and exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If include is set, only files that match this expression will be extracted. Files that match the exclude expression will never be extracted. Both parameters can be used in combination. Expressions are matched against stripped filenames as described above.
The Tar is closed afterwards. Reopen the file with open() again if you want to do additional operations.
Parameters
outdir(string) — the target directory for extractingstrip(?int|string) — either the number of path components or a fixed prefix to stripexclude(?string) — a regular expression of files to excludeinclude(?string) — a regular expression of files to include
Returns list[string]
Raises TarIOError
Raises TarCorruptedError when an entry’s name would place it
outside outdir, by being absolute or by climbing out through ...
Nothing past that entry is extracted.
Tar.create()
compress.tar.Tar.create(path: ?string)
Create a new Tar file.
If file is empty, the Tar file will be created in memory.
Parameters
path(?string)
Tar.add_file()
compress.tar.Tar.add_file(path: string, header: ?string|dict)
Add a file to the current Tar using an existing file in the filesystem.
Parameters
path(string) — path to the original fileheader(?string|dict) — either the name to use in Tar (string) or a dictionary oject with all meta data, empty to take from original.
Raises TarIOError
Tar.add_data()
compress.tar.Tar.add_data(data: bytes, header: ?string|dict)
Add a file to the current Tar using the given data as content.
If the header is set to nil or empty string, a file called
Untitled-{CURRENT_TIMESTAMP} will be created.
Parameters
data(bytes) — binary content of the file to addheader(?string|dict) — either the name to use in Tar (string) or a dictionary oject with all meta data
Raises TarIOError
Tar.close()
compress.tar.Tar.close()
Add the closing footer to the archive if in write mode, close all file handles
After a call to this function no more data can be added to the archive, for read access no reading is allowed anymore
“Physically, an archive consists of a series of file entries terminated by an end-of-archive entry, which consists of two 512 blocks of zero bytes”
Raises TarIOError
Tar.get_archive()
compress.tar.Tar.get_archive() -> bytes
Returns the created in-memory Tar data.
This implicitly calls close() on the Tar.
Returns bytes
Raises TarIOError
Raises TarIllegalCompressionError
Tar.save()
compress.tar.Tar.save(path: string) -> bool
Save the created in-memory Tar data
Note: It is more memory effective to specify the filename in the create() function and let the library work on the new file directly.
Parameters
path(string)
Returns bool
Tar.add_directory()
compress.tar.Tar.add_directory(directory: string, file_blacklist: ?list, ext_blacklist: ?list)
Adds the specified directory recursively to the archive and set’s it
path in the archive to dir.
Parameters
directory(string)file_blacklist(list) — if not empty, this function will ignore every file with a matching path.ext_blacklist(list) — if not empty, this function will ignore every file with a matching extension.
Raises TarIOError|Error
Tar.file_type()
compress.tar.Tar.file_type(f: string) -> int
Guesses the wanted compression from the given file.
Uses magic bytes for existing files, the file extension otherwise.
You don’t need to call this yourself. It’s used when you pass
COMPRESS_AUTO somewhere.
Parameters
file(string)
Returns int — (one of COMPRESS_BZIP, COMPRESS_GZIP or
COMPRESS_NONE)
2024, Richard Ore and The Zuri Contributors
compress.zip
import compress
compressexposes this ascompress.zip, soimport compressis enough and the names are called ascompress.zip.*.import compress.zipreaches the same definitions directly.
The zip module contains classes and functions to make working with zip
archives easy.
Constants
ZIP_FILE_MAX
compress.zip.ZIP_FILE_MAX = 4294967295
ZIP_FILE_COUNT_LIMIT
compress.zip.ZIP_FILE_COUNT_LIMIT: number = 65535
The maximum number of files in a zip archive when zip64 is not used
ZIP_MAX
compress.zip.ZIP_MAX: number
The maximum size of a zip archive when zip64 is not used
ZIP_EXT
compress.zip.ZIP_EXT: string = '.zip'
The default zip file extension
ZIP_STORED
compress.zip.ZIP_STORED: number = 0
Compression method that indicates no compression
ZIP_DEFLATE
compress.zip.ZIP_DEFLATE: number = 8
Compression method that indicates zlib Deflate compression
ZIP_BZIP2
compress.zip.ZIP_BZIP2: number = 12
Compression method that indicates Bzip2 compression
Functions
extract()
compress.zip.extract(file: string, destination: ?string, is_zip64: ?bool) -> bool
Extracts the zip archive at the file path to the given destination directory. If destination is not given, the file will be extracted into the current working directory.
This function returns true if the extraction was successful and
false otherwise.
NOTE: Set
is_zip64to true if the size of the zip file exceedsZIP_MAX.
Parameters
file(string)destination(?string) — Default value isos.cwd().is_zip64(?bool) — Default value isfalse.
Returns bool
compress()
compress.zip.compress(path: string, destination: ?string, compression_method: ?number, use_zip64: ?bool) -> bool
Compresses the given path (file or directory) into the destination zip archive.
When an Error is thrown because max size was exceeded, some files could have already been compressed. In this case, the zip archive will should still be usable but not all desired files will be contained in it.
NOTE: Set
use_zip64to true when compressing files exceedingZIP_FILE_MAXorZIP_FILE_COUNT_LIMIT
Parameters
file(string)destination(?string) — Default value isos.cwd().compression_method(?number) — Default value isZIP_DEFLATEis_zip64(?bool) — Default value isfalse.
Returns bool
Raises Error if file could not be written of zip max size
exceeded.
Classes
ZipItem
class compress.zip.ZipItem
ZipItem represents a single file or directory in a zip archive.
Fields
| Field | Type | Description |
|---|---|---|
name | string | Name of the file or directory |
directory | string | The directory in which the file or subdirectory belongs |
compression_method | string | The compression method for this file |
crc | string | The crc32 checksum for the file |
last_modified | Date | The last modified date for the file |
compressed_size | number | The size of the file as compressed in the archive. |
uncompressed_size | number | The size of the file when extracted from the archive |
is_encrypted | bool | If this file is encrypted or not. |
permission | number | The file permission |
error | string | Error encountered when attempting to read/extract the file |
data | bytes | The decompressed value of the zip item |
ZipItem.from_dict()
compress.zip.ZipItem.from_dict(dict: dict) -> ZipItem
Creates a new ZipItem from a dictionary. The dictionary should contain
the following keys: - name: string - dir: string — optional -
compress_method: number - crc: number - filemtime: number -
size_compressed: number - size_uncompressed: number - encrypted:
boolean - error: string — optional - data: bytes -
permission: number
Parameters
dict(dict)
Returns ZipItem
ZipItem.export()
compress.zip.ZipItem.export(base_dir: ?string) -> bool
Exports the ZipItem to file. If base_dir is given, the file will be exported into the base_dir and all ZipItem directories will be created inside of base_dir to reflect the ZipItem’s original structure.
This function returns true if the operation succeeds or false
otherwise.
Parameters
base_dir(?string) — Default value isos.cwd().
Returns bool
ZipFile
class compress.zip.ZipFile
ZipFile represents an instance of zip file.
Fields
| Field | Type | Description |
|---|---|---|
name | string | The name of the zip file |
last_modified | Date | The last modified date for the zip file |
time_created | Date | The time when the zip file was created |
size | number | The size of the zip file |
handle | file | The file handle for this zip file |
files | List<ZipItem> | A list of the ZipItems in the zip file |
ZipFile.export()
compress.zip.ZipFile.export(base_dir: ?string) -> bool
Exports the all files in the ZipFile to files on the machine. If base_dir is given, the files will be exported into the base_dir and all directories will be created inside of base_dir as is to reflect the ZipFile’s original structure.
This function returns true if the operation succeeds or false
otherwise.
Parameters
base_dir(?string) — Default value isos.cwd().
Returns bool
ZipArchive
class compress.zip.ZipArchive
ZipArchive provides a class for zip archive creation, manipulation and extraction.
Fields
| Field | Type | Description |
|---|---|---|
comment |
Constructor
compress.zip.ZipArchive(path: string, compression_method: ?number, use_zip_64: ?bool)
Parameters
path(string)compression_method(?number) — Default value isZIP_DEFLATEuse_zip_64(?bool) — Default value isfalse.
ZipArchive.create_dir()
compress.zip.ZipArchive.create_dir(name: string) -> bool
Adds a directory to the zip with the given name.
Parameters
name(string)
Returns bool
ZipArchive.create_file()
compress.zip.ZipArchive.create_file(path: string, data: bytes|string, stat: ?dict) -> bool
Adds a file to the path specified with the contents given data.
If the stat is given, it must be a valid dictionary derived from
file.stat().
Parameters
path(string)data(bytes|string)stat(?dict)
Returns bool
ZipArchive.add_file()
compress.zip.ZipArchive.add_file(path: string, destination: ?string) -> bool
Adds an existing file to the archive. If destination is given, the file will be written to the destination path in the archive.
Parameters
path(string)destination(?string)
Returns bool
ZipArchive.add_directory()
compress.zip.ZipArchive.add_directory(directory: string, file_blacklist: ?list, ext_blacklist: ?list) -> bool
Adds the specified directory recursively to the archive and set’s it
path in the archive to dir.
- If
file_blacklistis not empty, this function will ignore every file with a matching path. - Ifext_blacklistis not empty, this function will ignore every file with a matching.
Parameters
directory(string)file_blacklist(?list) — Default value is[]ext_blacklist(?list) — Default value is[]
Returns bool
ZipArchive.read()
compress.zip.ZipArchive.read() -> ZipFile
Reads the zip file in the specified path and returns a list of ZipFile describing it’s contents.
Parameters
path(string)
Returns ZipFile
ZipArchive.save()
compress.zip.ZipArchive.save() -> bool
Saves the current Zip archive to file.
Parameters
filename(string)
Returns bool
2022, Richard Ore and The Zuri Contributors
compress.zlib
import compress
Everything here is re-exported by
compress, soimport compressis enough and the names are called ascompress.*. Importingcompress.zlibon its own works too and reaches the same definitions.
This is the Zlib submodule for the compress module.
Constants
NO_COMPRESSION
compress.NO_COMPRESSION: number = 0
No compression level.
BEST_SPEED
compress.BEST_SPEED: number = 1
Best speed compression.
BEST_COMPRESSION
compress.BEST_COMPRESSION: number = 9
Best compression level.
DEFAULT_COMPRESSION
compress.DEFAULT_COMPRESSION: number
Default compression level.
FILTERED
compress.FILTERED: number = 1
Filtered compression strategy.
HUFFMAN_ONLY
compress.HUFFMAN_ONLY = 2
huffman only compression strategy
RLE
compress.RLE: number = 3
Rle compression strategy.
FIXED
compress.FIXED: number = 4
Fixed compression strategy.
DEFAULT_STRATEGY
compress.DEFAULT_STRATEGY: number = 0
Default compression strategy.
DEFAULT_MEMORY_LEVEL
compress.DEFAULT_MEMORY_LEVEL: number = 8
Default memory level
MAX_WBITS
compress.MAX_WBITS: number = 15
Maximum windows bit.
Functions
compress()
compress.compress(data, level, strategy, wbits, memory_level) -> bytes
Compress compresses as much data as possible, and stops when the input buffer becomes empty or the output buffer becomes full.
-
The compression
levelmust be DEFAULT_COMPRESSION, or between 0 and 9: 1 gives best speed, 9 gives best compression, 0 gives no compression at all (the input data is simply copied a block at a time). DEFAULT_COMPRESSION requests a default compromise between speed and compression (currently equivalent to level 6) -
The
wbitsparameter is the base two logarithm of the window size (the size of the history buffer). It should be in the range 8..15 for this version of the library. Larger values of this parameter result in better compression at the expense of memory usage. The default value is 15.
For the current implementation of compress(), a wbits value of 8 (a
window size of 256 bytes) is not supported. As a result, a request for 8
will result in 9 (a 512-byte window).
wbits can also be -8..-15 for raw compress. In this case, -wbits
determines the window size. compress() will then generate raw compress
data with no zlib header or trailer, and will not compute a check value.
wbits can also be greater than 15 for optional gzip encoding. Add 16
to wbits to write a simple gzip header and trailer around the
compressed data instead of a zlib wrapper. The gzip header will have no
file name, no extra data, no comment, no modification time (set to
zero), no header crc, and the operating system will be set to the
appropriate value, if the operating system can be determined by the
runtime.
For raw compress or gzip encoding, a request for a 256-byte window is rejected as invalid, since only the zlib header provides a means of transmitting the window size to the uncompressor.
-
The
strategyparameter is used to tune the compression algorithm. Use the value DEFAULT_STRATEGY for normal data, FILTERED for data produced by a filter (or predictor), HUFFMAN_ONLY to force Huffman encoding only (no string match), or RLE to limit match distances to one (run-length encoding). Filtered data consists mostly of small values with a somewhat random distribution. In this case, the compression algorithm is tuned to compress them better. The effect of FILTERED is to force more Huffman coding and less string matching; it is somewhat intermediate between DEFAULT_STRATEGY and HUFFMAN_ONLY. RLE is designed to be almost as fast as HUFFMAN_ONLY, but give better compression for PNG image data. The strategy parameter only affects the compression ratio but not the correctness of the compressed output even if it is not set appropriately. FIXED prevents the use of dynamic Huffman codes, allowing for a simpler decoder for special applications. -
The
memory_levelparameter specifies how much memory should be allocated for the internal compression state. memory_level 1 uses minimum memory but is slow and reduces compression ratio; memory_level 9 uses maximum memory for optimal speed. The default value is 8.
{.list}
Parameters
data(bytes|string)level(?int) — Default value isDEFAULT_COMPRESSION.strategy(?int) — Default value isDEFAULT_STRATEGY.wbits(?int) — Default value isMAX_WBITS.memory_level(?int) — Default value isDEFAULT_MEMORY_LEVEL.
Returns bytes
decompress()
compress.decompress(data, wbits) -> bytes
Decompress decompresses as much data as possible, and stops when the input buffer becomes empty or the output buffer becomes full.
-
In this implementation, decompress() always flushes as much output as possible to the output buffer, and always uses the faster approach on the first call.
-
The
wbitsparameter is the base two logarithm of the maximum window size (the size of the history buffer). It should be in the range 8..15 for this version of the library. The default value is
wbitsmust be greater than or equal to thewbitsvalue provided to compress() while compressing, or it must be equal to 15 if compress() is used with the default values. If a compressed stream with a larger window size is given as input, decompress() will return with the error code data error instead of trying to allocate a larger window.
wbits can also be zero to request that decompress use the window size
in the zlib header of the compressed stream.
wbits can also be -8..-15 for raw decompress. In this case, -wbits
determines the window size. decompress() will then process raw compress
data, not looking for a zlib or gzip header, not generating a check
value, and not looking for any check values for comparison at the end of
the stream. This is for use with other formats that use the compress
compressed data format such as zip. Those formats provide their own
check values. If a custom format is developed using the raw compress
format for compressed data, it is recommended that a check value such as
an Adler-32 or a CRC-32 be applied to the uncompressed data as is done
in the zlib, gzip, and zip formats. For most applications, the zlib
format should be used as is. Note that comments on the use in compress()
applies to the magnitude of wbits.
wbits can also be greater than 15 for optional gzip decoding. Add 32
to wbits to enable zlib and gzip decoding with automatic header
detection, or add 16 to decode only the gzip format (the zlib format
will return a data error). decompress() will not automatically decode
concatenated gzip streams.
- decompress() can decompress either zlib-wrapped or
gzip-wrapped compress data. If the compression uses gzip-wrapper, the
correct
wbitsmay need to be set.
Parameters
data(bytes|string)wbits(?int) — Default value isMAX_WBITS.
Returns bytes
Classes
ZlibEncoder
class compress.ZlibEncoder < GzipEncoder
A streaming Deflate encoder.
Example:
%> import compress.zlib
%> var encoder = zlib.ZlibEncoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(78 01 cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00 1a 0b 04 5d)
Constructor
compress.ZlibEncoder(level)
Creates a new streaming Deflate encoder at the given level (0..=9).
Parameters
level(?number) — Default 1
ZlibDecoder
class compress.ZlibDecoder < GzipDecoder
Streaming zlib decompressor implemention. This decoder reads standard zlib blocks and frames produced at any zlib compression level.
Wraps a byte stream of compressed data and yields decompressed bytes.
Example:
%> import compress.zlib
%> var data = zlib.compress('hello world')
%> data
(78 9c cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00 1a 0b 04 5d)
%>
%> var decoder = zlib.ZlibDecoder(data)
%> decoder.read_as_string()
hello world
This decoder is level-independent and supports standard zlib frames produced by all zlib compression levels.
Constructor
compress.ZlibDecoder(source)
Creates a Zlib decoder
Parameters
source(bytes)
2021, Richard Ore and Zuri contributors
compress.zstd
import compress
compressexposes this ascompress.zstd, soimport compressis enough and the names are called ascompress.zstd.*.import compress.zstdreaches the same definitions directly.
This is the Zstd submodule for the compress module. It implements zstd compression levels -8 through 4 (Fast and DFast strategies), targeting high-speed compression for data transfers. It produces standard zstd frames decompressible by any compliant decoder.
Functions
compress()
compress.zstd.compress(data, level) -> bytes
Compress data using the default options for Zstd.
Parameters
data(bytes|string)level(?number)
Returns bytes
decompress()
compress.zstd.decompress(data) -> bytes
Decompress a Zstd compressed data.
Parameters
data(bytes|string)
Returns bytes
Classes
ZstdEncoder
class compress.zstd.ZstdEncoder
Streaming zstd compressor implemention. This encoder supports levels -8 through 4 for fast transfer pipelines.
Buffers input until a full block (128 KiB) is ready, then compresses and
writes it to the underlying writer. Call finish() to flush the final
block, write the content checksum, and recover the writer.
Internal buffers (hash tables, sequence scratch, block encoder
workspace) are allocated once and reused across blocks. To reuse them
across multiple frames, call reset() instead of finish.
Example:
%> import compress.zstd
%> var encoder = zstd.ZstdEncoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(28 b5 2f fd 04 00 59 00 00 68 65 6c 6c 6f 20 77 6f 72 6c 64 68 69 1e b2)
The normal match finder operates within a sliding window (512 KiB at
L1). LDM (Long Distance Matching) finds matches at distances up to 1 << window_log
bytes by sampling positions into a separate hash table. Useful for data
with long-range repeats: log files, database dumps, source archives.
window_log controls the maximum match distance (24 = 16 MiB, 27 = 128
MiB). The main cost is memory: the encoder allocates an 8 MiB LDM hash
table plus a window buffer of 1 << window_log bytes, and the decoder
allocates a window buffer of the same size (declared in the frame
header). At window_log 27 that is ~136 MiB on each side. On data without
long-range repeats, LDM adds overhead with no ratio benefit.
ZstdEncoder own persistent hash tables and workspace buffers. Call
reset() to start a new frame while reusing all allocations.
Constructor
compress.zstd.ZstdEncoder(level, window_log, ldm)
Creates a new streaming Zstd encoder at the given level (-8..=4).
Negative levels (-8 through -1) unlocks zstd’s fastest compression tiers. They can be useful when throughput matters more than ratio.
Level 0 maps to the module’s default which is currently level 1.
Positive levels spend more match-finding work for better ratios while staying in the Fast/DFast range.
Parameters
level(?number) — Default 1window_log(?number) — Default 10ldm(?bool) — Default false
ZstdEncoder.finish()
compress.zstd.ZstdEncoder.finish() -> bytes
Flushes remaining data, writes the content checksum, and returns the inner byte stream.
Returns bytes
Note: Once finish is called, the encoder can no longer be reused.
ZstdEncoder.close()
compress.zstd.ZstdEncoder.close() -> bytes
Same as finish() for file API compartibility.
Returns bytes
Note: Once finish is called, the encoder can no longer be reused.
ZstdEncoder.reset()
compress.zstd.ZstdEncoder.reset() -> bytes
Finishes the current frame and installs byte stream for the next one.
Returns the previous byte stream containing the completed frame. All internal buffers (hash tables, workspace, block scratch) stay allocated and are reused for the next frame.
Returns bytes
ZstdEncoder.write()
compress.zstd.ZstdEncoder.write(data) -> number
Writes data into this encoder’s byte stream, returning how many bytes were written.
Parameters
data(bytes|string)
Returns number
ZstdEncoder.write_all()
compress.zstd.ZstdEncoder.write_all(data)
Attempts to write an entire data into this encoder’s byte stream.
Parameters
data(bytes|string)
ZstdEncoder.flush()
compress.zstd.ZstdEncoder.flush()
Flushes this output stream, ensuring that all intermediately buffered contents reach their destination.
ZstdDecoder
class compress.zstd.ZstdDecoder
Streaming zstd decompressor implemention. This decoder reads standard zstd blocks and frames produced at any zstd compression level.
Wraps a byte stream of compressed data and yields decompressed bytes. Supports multi-frame streams and skippable frames.
Example:
%> import compress.zstd
%> var data = zstd.compress('hello world')
%> data
(28 b5 2f fd 24 0b 59 00 00 68 65 6c 6c 6f 20 77 6f 72 6c 64 68 69 1e b2)
%>
%> var decoder = zstd.ZstdDecoder(data)
%> decoder.read_as_string()
hello world
ZstdDecoder own persistent hash tables and workspace buffers. Call
reset() to start a new frame while reusing all allocations.
This decoder is level-independent and supports standard zstd frames produced by all zstd compression levels.
Constructor
compress.zstd.ZstdDecoder(source)
Creates a Zstd decoder
Parameters
source(bytes)
ZstdDecoder.reset()
compress.zstd.ZstdDecoder.reset(new_source)
Installs a new data source for the next frame, keeping all internal buffers allocated.
Parameters
new_source(bytes)
ZstdDecoder.close()
compress.zstd.ZstdDecoder.close()
Closes the decoder by resetting it into an empty stream.
ZstdDecoder.read()
compress.zstd.ZstdDecoder.read(length) -> bytes
Reads some bytes up to the amount of bytes specified by length from the current source. Returns an empty byte stream when there is no more data to read.
This function does not block or wait waiting for data, but reads as much data as is available to read when it runs.
Parameters
length(number)
Returns bytes
ZstdDecoder.read_exact()
compress.zstd.ZstdDecoder.read_exact(length) -> bytes
Reads the exact number of bytes from the buffer. This method will raise
an error if it encounters an unexpected EOF (end of file) or there are
insufficient data to read to complete the required number of bytes.
Parameters
length(number)
Returns bytes
ZstdDecoder.read_all()
compress.zstd.ZstdDecoder.read_all() -> bytes
Reads all remaining bytes until EOF is encountered in the source.
Returns bytes
ZstdDecoder.read_as_string()
compress.zstd.ZstdDecoder.read_as_string() -> string
Reads all remaining bytes until EOF is encountered in the source and returns the data read as a string instead of a byte stream.
Returns string
2021, Richard Ore and Zuri contributors
isolate
import isolate
High-throughput parallel concurrency backed by a pool of operating-system isolate threads.
Architectural Model: Shared-Nothing Isolates
Modern multi-core computing demands concurrency models that scale linearly with CPU core count while safeguarding applications against data races and memory corruption. Zuri addresses this by adopting a Shared-Nothing Isolate architecture (similar to the Actor model and Dart/Erlang isolates) rather than shared-memory green threads.
In Zuri, every isolate thread executes within its own self-contained
Virtual Machine (VM) instance. Each isolate possesses an independent
generational garbage-collected heap, nursery allocator, call frame
stack, register array, and global module namespace. Isolates run
concurrently across physical CPU cores without sharing heap pointers or
mutable structures with other isolates or the main thread.
Inter-isolate communication is accomplished strictly across defined
boundary barriers. When values, closures, or data structures are
transmitted via spawn(), join(), or Channel queues, Zuri traverses
the object graph and constructs an isomorphic copy on the destination
isolate’s heap. This transfer barrier faithfully preserves cyclic
graphs, shared object identity within the payload, and class prototypes
without sharing mutable memory across thread boundaries.
Memory Footprint & Task Density
High-concurrency systems frequently face a dilemma between heavy operating system threads and complex coroutine runtimes. Zuri reconciles high task density with OS-level multi-core execution through a two-tier scheduling design:
Instead of allocating a dedicated OS thread or a dedicated runtime stack
for every dispatched task, calling isolate.spawn() creates a
lightweight Task descriptor (the callee and its arguments, already
captured into a heap-independent snapshot) and pushes it onto a shared,
mutex-guarded queue.
- Isolate Pooling & Reuse: A fixed pool of isolate OS
threads, configured via
configure()and defaulting to the system’s available CPU core count is pre-warmed at startup. These isolate threads continuously drain the task queue, executing tasks sequentially within their long-lived isolate contexts. - Small, Fixed-Size Task Descriptors: Because a queued task is a captured value snapshot rather than a suspended OS thread, applications can enqueue large task bursts (e.g. 100,000+ jobs) without paying for a dedicated stack or heap per task. The fixed part of a task (its queue slot plus its join handle) is a few hundred bytes; a task’s actual total footprint on top of that scales with whatever the callee’s own arguments and captured state need to carry across the boundary, same as any other value transfer described above.
Garbage Collection & Scalability Under Load
A frequent performance bottleneck in multi-threaded runtimes is garbage collection cross-talk. When multiple threads share a single global heap, an allocation spike in one thread can trigger global Stop-The-World (STW) pauses, lock contention on shared allocation arenas, or heavy write-barrier synchronization that degrades throughput across all CPU cores.
Zuri eliminates GC interference through complete heap segregation:
- Local Generational Collections: Every isolate isolate manages its own nursery and old-generation heaps. Young-generation minor collections and major mark-sweep cycles execute entirely on the local isolate thread. - Zero Stop-The-World Pauses Across Threads: A garbage collection cycle on Isolate A never halts, pauses, or synchronizes with Isolate B or the main thread. Compute-heavy, allocation-intensive workloads (such as parallel tree building, matrix algebra, and image rendering) achieve near-linear multi-core speedup without GC contention.
Data Safety & Race Conditions
Concurrency bugs in shared-memory architectures such as data races, torn reads, and lock inversion deadlocks often arise from uncontrolled concurrent access to mutable heap memory.
- Data-Race Free by Construction: Because memory is physically partitioned into isolated heaps, two isolates can never concurrently mutate the same object in memory. Data races on heap objects are fundamentally impossible, eliminating the need for mutex locks or synchronized wrappers around user data structures. - Snapshot Closure Semantics: When a closure is passed to an isolate, its captured local variables and referenced global definitions are snapshot at dispatch time. The isolate receives an isolated clone of the captured state; subsequent mutations performed by the caller never affect the isolate, and isolate mutations never leak back to the caller.
Comparison with Go Goroutines
Both Go and Zuri provide lightweight concurrency abstractions, but they optimize for different runtime trade-offs:
Go utilizes Communicating Sequential Processes (CSP) built on M:N green threads multiplexed across OS threads with a single shared global heap. Zuri utilizes Thread-Pooled Shared-Nothing Isolates with deep-copy transfer barriers.
| Feature | Go Goroutines | Zuri Isolates |
|---|---|---|
| Concurrency Paradigm | M:N green threads on a single shared heap | Thread-pooled shared-nothing isolates / actors |
| Memory Safety | Shared memory; data races possible without mutexes | Data-race free by design via physical isolation |
| GC Impact | Shared global GC cycles and allocator write barriers | Independent per-isolate GC; zero STW cross-talk |
| Task Density | ~2 KB per goroutine stack | a few hundred bytes of fixed queue/handle overhead per task; pooled thread reuse |
| Communication | Shared-memory pointer channels or locks | Deep-copied value graphs or moved native resources |
| Primary Strengths | Ultra-high-concurrency non-blocking I/O multiplexing | Compute-heavy parallel processing & task pipelines |
Values That Can Cross Isolate Boundaries
Zuri’s value transfer engine supports virtually all language data types across isolate boundaries, including:
- Primitives:
nil,bool,number,bigint,string,bytes, andrange. - Collections:listanddict(including nested, multidimensional, and cyclic structures). - Object-Oriented Structures: Class instances, class declarations (preserving static fields, superclasses, and method tables), and bound methods. - Executable Code: Functions, closures (capturing upvalues), anonymous lambdas, and top-level scripts. - Modules: imported modules and module bindings, including one a closure captured.
- Native Handles:
Ptrhandles (moved linearly),Channelhandles, andIsolatehandles.
A module is reloaded on the receiving isolate rather than copied, so the recipient gets that isolate’s own instance of it, with its own top-level state.
Non-Transferable Types
Attempting to transmit an active operating system file handle
(file(...)) across a isolate boundary raises an IsolateError.
Native Resources & Ownership Transfer
Operating system resources encapsulated within native pointers
(Ptr)—such as database connections, network sockets, or compression
streams—cannot be safely duplicated across threads.
Zuri enforces linear ownership transfer (move semantics) for Ptr
values: - When a Ptr is sent through spawn(), join(), or a
Channel, ownership is transferred to the recipient, and the sender’s
handle is permanently invalidated.
- Subsequent attempts to read or use an invalidated
Ptrwill raise an error. - In contrast, synchronization primitives likeChannelandIsolatehandles are thread-safe and may be freely shared among multiple isolates.
Failure Isolation & Fault Tolerance
A software defect or unhandled exception within one isolate is strictly contained and will never terminate peer isolates, corrupt the pool, or crash the host process.
- Structured Error Propagation: Raised exceptions in a
isolate task are captured and re-raised as
IsolateError(complete with original source file and line stack traces) whenjoin()ortry_join()is invoked. - Automatic Isolate Recycling: In the rare event of an unrecoverable isolate panic, the affected isolate thread is safely retired and replaced with a fresh isolate, maintaining full pool capacity. - Un-Joined Failure Diagnostics: If an isolate fails and its handle is dropped without being joined, a diagnostic warning is emitted to ensure errors are never silently swallowed.
Cancellation Semantics
Applications can cancel running or queued isolates via
Isolate.cancel().
- Preemptive Blocking Interruption: If an isolate is
blocked on a channel operation (
Channel.send(),Channel.recv()), an isolate join (Isolate.join()), or a multiplexer (select(),wait_any(),wait_all()),cancel()interrupts the blocking call promptly with aIsolateCancelledError. - Cooperative Compute Polling: During active computational loops, code can cooperatively pollisolate.is_cancelled()to gracefully terminate execution.
Structured Concurrency (isolate.scope())
Unbounded background concurrency can lead to orphan tasks, leaked
resources, and missed errors if spawned isolates outlive their intended
context. Zuri provides Structured Concurrency through
isolate.scope(body) and the Scope handle.
- Lifetime Bounded to Lexical Scope: Any isolate spawned
via
s.spawn()within ascope()block is guaranteed to complete beforescope()returns. - Fail-Fast Automatic Cancellation: If the scope’sbodyraises an exception or if any child isolate fails, all other sibling isolates in that scope are automatically sent a cancellation signal (cancel()), preventing wasted computation. - Clean Teardown & Error Prioritization:scope()waits for all cancelled siblings to finish shutting down before propagating the root failure to the caller.
Examples
Basic Task Spawning
import isolate
def calculate_primes(limit) {
var count = 0
iter var n = 2; n <= limit; n++ {
var is_prime = true
iter var d = 2; d * d <= n; d++ {
if n % d == 0 {
is_prime = false
break
}
}
if is_prime count++
}
return count
}
var task = isolate.spawn(calculate_primes, 100_000)
echo "Primes found: ${task.join()}"
Parallel Batch Mapping
import isolate
def square(n) {
return n * n
}
var numbers = [1, 2, 3, 4, 5, 6, 7, 8]
var results = isolate.map(square, numbers)
echo "Squares: ${results}"
Structured Concurrency (scope)
import isolate
var total = isolate.scope(def(s) {
var w1 = s.spawn(def() { return 10 * 2 })
var w2 = s.spawn(def() { return 20 * 2 })
return w1.join() + w2.join()
})
echo "Total: ${total}"
Publish-Subscribe (Broadcast)
import isolate
var hub = isolate.broadcast()
var sub1 = hub.subscribe()
var sub2 = hub.subscribe()
isolate.spawn(def(c) { echo "Isolate 1: ${c.recv()}" }, sub1)
isolate.spawn(def(c) { echo "Isolate 2: ${c.recv()}" }, sub2)
hub.send("Hello Subscribers!")
hub.close()
The isolate API
Every public name in isolate, wherever it is declared. Each links to
the page that documents it.
| Name | Kind | Summary |
|---|---|---|
isolate.Broadcast | class | A one-to-many publish-subscribe message distribution hub. |
isolate.Channel | class | A thread-safe, multi-producer multi-consumer (MPMC) message queue. |
isolate.Isolate | class | A handle to an isolate, returned by spawn(). |
isolate.IsolateCancelledError | class | Raised from Isolate.join(), Channel.send(), Channel.recv(), isolate.wait_any(), isolate.wait_all(),… |
isolate.IsolateError | class | Raised when an isolate’s function raises an uncaught error, a Channel operation fails, or a value cannot… |
isolate.IsolateTimeoutError | class | Raised when Isolate.join(), Channel.send(), or Channel.recv() is given a timeout and it elapses… |
isolate.Scope | class | A structured concurrency supervisor and task nursery. |
isolate.active_count | function | |
isolate.broadcast | function | Creates a new one-to-many Broadcast distribution hub. |
isolate.channel | function | Creates a new thread-safe Channel. |
isolate.configure | function | Sets how many isolate threads the pool may run at once. |
isolate.cpu_count | function | |
isolate.is_cancelled | function | Checks whether the currently running isolate has been asked to stop via its handle’s cancel(). |
isolate.is_shutdown | function | |
isolate.map | function | Runs fn once per item in items, in parallel across the isolate pool, and returns the results in the same… |
isolate.pool_size | function | How many isolate threads the pool may run at once. |
isolate.queued_count | function | |
isolate.scope | function | Executes body(s) within a structured concurrency scope. |
isolate.select | function | Multiplexes across multiple channels, blocking until at least one channel is ready to deliver a value or is… |
isolate.shutdown | function | Stops the pool from accepting any further spawn() calls, then blocks until every isolate already spawned;… |
isolate.spawn | function | |
isolate.spawn_named | function | Same as spawn(), but gives the isolate a name; purely a debugging label, read back via Isolate.name()… |
isolate.started_count | function | How many isolate threads the pool has started so far. |
isolate.wait_all | function | Blocks until every one of isolates has finished, then returns their results in the same order as isolates. |
isolate.wait_any | function | Blocks until at least one of isolates finishes, and returns that one. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
isolate.broadcast | isolate.broadcast.* | One-to-many (fan-out) publish-subscribe message distribution for isolate isolates. |
isolate.channel | isolate.channel.* | Thread-safe, multi-producer multi-consumer (MPMC) communication queues for passing values and messages across… |
isolate.error | isolate.error.* | IsolateError, kept in its own leaf module (no imports of its own) so both index and channel can depend… |
Functions
spawn()
isolate.spawn(fn: function, ...args: list)
spawn_named()
isolate.spawn_named(name: string, fn: function, ...args: list) -> Isolate
Same as spawn(), but gives the isolate a name; purely a debugging
label, read back via Isolate.name() and included in the “unobserved
failure” warning (see the module docs’ “Failure isolation” section) if
this one fails and is never join()ed or try_join()ed.
Parameters
name(string)fn(function) — a closure, bound method, or class method.args(...any)
Returns Isolate
Raises TypeError if fn is not callable
Raises IsolateError if fn is a native function or class,
fn/args cannot cross an isolate boundary, or the pool has been
shutdown()
scope()
isolate.scope(body: function)
Executes body(s) within a structured concurrency scope.
isolate.scope() establishes a structured task boundary, guaranteeing
that all isolates spawned via s.spawn() or s.spawn_named() finish
before scope() returns.
Lifecycle & Error Semantics
- Completion Guarantee:
scope()blocks untilbody(s)returns and all child isolates have terminated. 2. Automatic Cancellation: Ifbodyraises an exception or if any child isolate fails, all other still-running children in the scope are automatically sent a cancellation signal (cancel()). 3. Graceful Teardown:scope()waits for all cancelled isolates to cleanly finish their teardown before returning or re-raising. 4. Error Priority: Ifbodyraised an exception,body’s exception is re-raised. Otherwise, the earliest child failure (excluding secondaryIsolateCancelledErrorcascades) is re-raised.
import isolate
var result = isolate.scope(def(s) {
var task1 = s.spawn(def() { return 100 })
var task2 = s.spawn(def() { return 200 })
return task1.join() + task2.join()
})
echo "Total: ${result}" # 300
Parameters
body(function) — A callback functiondef(s)that receives a freshScope.
Returns — any: The return value of body(s).
Raises IsolateError if a child isolate failed and body did not
raise.
Raises any: Any exception raised by body(s).
configure()
isolate.configure(threads: int)
Sets how many isolate threads the pool may run at once. Has no effect if the pool has already started; its size is fixed once the first isolate runs.
The size is a ceiling. A thread is started only when an isolate is waiting and every thread already started is busy, so a pool sized for peak load costs only the threads the load actually needed.
Parameters
threads(int)
Returns — bool: true if applied, false if the pool had already
started.
Raises ValueError if threads is less than 1
pool_size()
isolate.pool_size() -> int
How many isolate threads the pool may run at once. Starts the pool, with
whatever size configure() set or the machine’s CPU count otherwise, if
it has not started yet.
Returns int
started_count()
isolate.started_count() -> int
How many isolate threads the pool has started so far. Threads start as
isolates need them, so this grows with the most isolates ever waiting at
once and never passes pool_size().
Returns int
cpu_count()
isolate.cpu_count()
Returns — int: the number of logical CPUs this machine reports.
shutdown()
isolate.shutdown(timeout: ?number)
Stops the pool from accepting any further spawn() calls, then blocks
until every isolate already spawned; queued or already running; has
finished. Nothing in flight is abandoned.
Permanent: once called, every spawn() for the rest of the process
raises IsolateError, even if this call itself later times out.
Parameters
timeout(number) — seconds to wait before giving up. Waits indefinitely if omitted.
Raises IsolateTimeoutError if timeout elapses with isolates
still in flight
active_count()
isolate.active_count()
Returns — int: isolates an isolate is actively running right now.
Does not include ones still queued_count().
queued_count()
isolate.queued_count()
Returns — int: isolates spawned but not yet picked up by a isolate.
is_shutdown()
isolate.is_shutdown()
Returns — bool: true if shutdown() has been called.
wait_any()
isolate.wait_any(isolates: list, timeout: ?number)
Blocks until at least one of isolates finishes, and returns that one.
Useful for reacting to whichever of several tasks completes first,
rather than joining them one at a time in a fixed order.
Parameters
isolates([Isolate]) — must not be empty.timeout(number) — seconds to wait before giving up. Waits indefinitely if omitted.
Returns — Isolate: whichever one finished first.
Raises ValueError if isolates is empty
Raises IsolateTimeoutError if timeout elapses before any of them
finish
Raises IsolateCancelledError if the CALLING isolate is cancelled
while blocked here
wait_all()
isolate.wait_all(isolates: list, timeout: ?number) -> list
Blocks until every one of isolates has finished, then returns their
results in the same order as isolates.
Parameters
isolates([Isolate]) — may be empty (returns[]).timeout(number) — seconds to wait for all of them combined, not per isolate. Waits indefinitely if omitted.
Returns list
Raises IsolateError from the first one (in isolates order) whose
function raised, once every one of them has finished
Raises IsolateTimeoutError if timeout elapses before all of them
finish
Raises IsolateCancelledError if the CALLING isolate is cancelled
while blocked here
map()
isolate.map(fn: function, items: list, timeout: ?number) -> list
Runs fn once per item in items, in parallel across the isolate pool,
and returns the results in the same order as items. Equivalent to
spawning an isolate per item and passing the handles to wait_all().
Parameters
fn(function) — a closure, bound method, or class method.items(list)timeout(number) — seconds to wait for the whole batch combined. Waits indefinitely if omitted.
Returns list
Raises IsolateError from the first item (in items order) whose
call raised, once every call has finished
Raises IsolateTimeoutError if timeout elapses before every call
finishes
Raises IsolateCancelledError if the CALLING isolate is cancelled
while blocked here
is_cancelled()
isolate.is_cancelled() -> bool
Checks whether the currently running isolate has been asked to stop via
its handle’s cancel(). Meant to be polled periodically by long-running
or looping isolate code so it can return early.
Calling this outside an isolate (from the main script, or from another
thread) always returns false.
Returns bool
Classes
Isolate
class isolate.Isolate
A handle to an isolate, returned by spawn().
Constructor
isolate.Isolate(ptr)
Isolate.join()
isolate.Isolate.join(timeout: ?number) -> any
Blocks until the isolate finishes and returns its result. May be called more than once; later calls return immediately with the same result.
Parameters
timeout(number) — seconds to wait before giving up. Waits indefinitely if omitted.
Returns any
Raises IsolateError if the isolate’s function raised an uncaught
error
Raises IsolateTimeoutError if timeout elapses first
Raises IsolateCancelledError if the CALLING isolate (not this one)
is cancelled while blocked here
Isolate.cancel()
isolate.Isolate.cancel()
Requests that the isolate stop. Has no effect on an isolate that has already finished.
If the isolate is currently blocked in Channel.send()/ recv(),
Isolate.join(), wait_any(), wait_all(), or select(), that call
is interrupted within roughly 50ms and raises IsolateCancelledError.
Otherwise; plain, non-blocking code; cancellation is cooperative: it
does not interrupt code that’s already running. The isolate’s own
function must check isolate.is_cancelled() and return on its own.
Isolate.is_cancelled()
isolate.Isolate.is_cancelled()
Returns — bool: true if cancel() has been called on this
isolate.
Isolate.name()
isolate.Isolate.name()
Returns — ?string: the name given via spawn_named(), or nil if
this isolate was started with plain spawn().
Isolate.try_join()
isolate.Isolate.try_join() -> any
Returns the isolate’s result, or nil if it has not finished yet. Does
not block.
Returns any
Raises IsolateError if the isolate’s function raised an uncaught
error
Note: an isolate that returns
nilis indistinguishable from one still running. Useis_done()to tell them apart.
Isolate.is_done()
isolate.Isolate.is_done()
Returns — bool: true if the isolate has finished.
Isolate.status()
isolate.Isolate.status()
Unlike try_join(), never marks a failure as “observed”; safe to poll
purely to check progress without silencing the warning a failure that’s
never actually join()ed/try_join()ed would otherwise get (see the
module docs’ “Failure isolation” section).
Returns — string: 'pending' while still running, 'done' once
finished successfully, or 'error' once finished with an uncaught
error.
Isolate.handle()
isolate.Isolate.handle()
This isolate’s underlying native handle, needed by wait_any() to
operate on a plain list of Isolates from outside the class. Not meant
for direct use.
Scope
class isolate.Scope
A structured concurrency supervisor and task nursery.
Scope manages the lifecycle of a set of concurrent child isolates
spawned within a isolate.scope() block. It guarantees that:
- Lifetime Bounding: All isolates spawned via
s.spawn()ors.spawn_named()are tracked by the scope and cannot outlive the enclosingscope(body)block. 2. Fail-Fast Error Handling: If any child isolate raises an unhandled error (or if thescopebody itself fails), all remaining sibling isolates are automatically cancelled (cancel()), preventing wasted computation. 3. Deterministic Teardown:scope()waits for all active and cancelled children to complete execution before re-raising the root failure or returning the body’s result.
Scope instances cannot be constructed directly; they are created and
passed to the callback function by isolate.scope(body).
Examples
Parallel Task Aggregation
import isolate
var summary = isolate.scope(@(s) {
var task_a = s.spawn(@() { return "Task A complete" })
var task_b = s.spawn(@() { return "Task B complete" })
return "${task_a.join()} & ${task_b.join()}"
})
echo summary # "Task A complete & Task B complete"
Fail-Fast Child Cancellation
import isolate
catch {
isolate.scope(def(s) {
# Spawn an isolate that fails quickly
s.spawn(def() {
raise "critical error in child"
})
# Sibling isolate is automatically cancelled as soon as child 1 fails
s.spawn(def() {
iter var i = 0; i < 1_000_000; i++ {
if isolate.is_cancelled() break
}
})
})
} as err {
echo "Scope caught child failure: ${err.message}"
}
Constructor
isolate.Scope()
Scope.spawn()
isolate.Scope.spawn(fn: function, ...args: list)
Spawns a concurrent isolate task tracked by this scope.
The spawned isolate is bound to this scope’s lifecycle:
isolate.scope() will not return until this isolate has completed. If
this isolate fails, all sibling isolates in the scope are automatically
cancelled.
isolate.scope(def(s) {
var w = s.spawn(def(x, y) { return x + y }, 10, 20)
echo "Result: ${w.join()}" # 30
})
Parameters
fn(function) — A callable function, closure, or method to execute.args(...any) — Arguments to pass tofn.
Returns — Isolate: A handle to the spawned isolate.
Raises TypeError if fn is not callable.
Raises IsolateError if fn is a native function or class, if
fn/args cannot cross isolate boundaries, or if the isolate pool has
shut down.
Scope.spawn_named()
isolate.Scope.spawn_named(name: string, fn: function, ...args: list)
Spawns a named concurrent isolate task tracked by this scope.
Operates identically to Scope.spawn(), but assigns an identifier
name used in diagnostic error messages and debugging traces.
isolate.scope(def(s) {
var w = s.spawn_named("fetch-user", fetch_user, user_id)
return w.join()
})
Parameters
name(string) — A human-readable identifier for the isolate.fn(function) — A callable function, closure, or method to execute.args(...any) — Arguments to pass tofn.
Returns — Isolate: A handle to the spawned isolate.
Raises TypeError if fn is not callable.
Raises IsolateError if fn is a native function or class, if
fn/args cannot cross isolate boundaries, or if the isolate pool has
shut down.
Scope.children()
isolate.Scope.children()
Returns all Isolate handles spawned within this scope.
isolate.scope(def(s) {
s.spawn(task1)
s.spawn(task2)
for child in s.children() {
echo "Child isolate active"
}
})
Returns — list: A list of Isolate instances in the order they were
spawned.
2026, Richard Ore and Zuri contributors
isolate.broadcast
import isolate.broadcast
isolatelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledisolate.broadcast.*needsimport isolate.broadcast.
One-to-many (fan-out) publish-subscribe message distribution for isolate isolates.
Overview
While a standard Channel provides one-to-one or many-to-one message
delivery (where each sent message is consumed by exactly one receiver),
a Broadcast hub provides one-to-many publish-subscribe semantics.
Every value published via Broadcast.send() is cloned and delivered to
all currently active subscriber channels.
Broadcast is built on top of Zuri’s thread-safe Channel primitives,
inheriting the same deep-copy memory isolation guarantees: published
values cross isolate barriers cleanly without shared mutable state.
Re-exported by the isolate module, so import isolate is sufficient
to use isolate.Broadcast and isolate.broadcast().
Subscription & Message Delivery
- Dynamic Subscription: Calling
broadcast.subscribe()registers a new subscriber and returns a dedicatedChannelfor that subscriber. - No Historical Replay: Subscribers only receive messages published after they subscribe. Past messages published prior to subscription are not replayed. - Independent Consumer Queues: Each subscriber possesses its own queue buffer. One slow consumer reading slowly from its channel does not prevent fast consumers from receiving their copies at full speed.
Backpressure & Drop-on-Full Semantics
- Unbounded Subscriber Channels (
capacity: 0ornil): Each subscriber channel grows dynamically. Published messages are always delivered to all subscribers without dropping. - Bounded Subscriber Channels (capacity: n > 0): When a subscriber’s individual channel buffer reaches capacityn, the broadcaster uses a non-blocking send (timeout: 0). If the buffer is full, the message is dropped specifically for that slow subscriber rather than blocking the publisher or stalling other subscribers. This prevents a single lagging isolate from causing system-wide backpressure or deadlocks.
Lifecycle & Graceful Teardown
- Unsubscribing: A subscriber can be removed by calling
broadcast.unsubscribe(ch). The channel stops receiving new broadcasts, but any previously queued messages remain readable. - Closing: Callingbroadcast.close()closes the broadcast hub and all active subscriber channels. Subscribed isolates will drain their remaining buffered messages, after which subsequentrecv()calls returnnil. Attempting to callsend()orsubscribe()on a closed broadcast raises aIsolateError. - Automatic Pruning: If a subscriber closes its channel independently, the broadcast hub automatically detects the closed channel during the nextsend()and prunes it from the subscriber registry.
Examples
1. Publish-Subscribe Event Fan-Out
import isolate
var hub = isolate.broadcast()
# Isolate 1: Logging service
var sub1 = hub.subscribe()
var logger = isolate.spawn(def(ch) {
while true {
var msg = ch.recv()
if msg == nil break
echo "[Logger] ${msg}"
}
}, sub1)
# Isolate 2: Metrics service
var sub2 = hub.subscribe()
var metrics = isolate.spawn(def(ch) {
var count = 0
while true {
var msg = ch.recv()
if msg == nil break
count++
}
return count
}, sub2)
# Broadcast events to all subscribers
hub.send("event: user_login")
hub.send("event: page_view")
hub.close()
logger.join()
echo "Total metrics events: ${metrics.join()}"
2. Dynamic Unsubscription
import isolate
var hub = isolate.broadcast()
var sub = hub.subscribe()
hub.send("first message")
echo sub.recv() # "first message"
hub.unsubscribe(sub)
hub.send("second message") # Not delivered to sub
echo sub.try_recv() # nil
hub.close()
Functions
broadcast()
isolate.broadcast(capacity: ?int) -> Broadcast
Creates a new one-to-many Broadcast distribution hub.
import isolate
var hub = isolate.broadcast()
Parameters
capacity(?int) — Per-subscriber buffer capacity (nilor0means unbounded).
Returns Broadcast
Classes
Broadcast
class isolate.Broadcast
A one-to-many publish-subscribe message distribution hub.
Constructor
isolate.Broadcast(capacity: ?int)
Constructs a new Broadcast hub.
import isolate
var hub = isolate.broadcast() # Unbounded subscriber buffers
var bounded_hub = isolate.broadcast(50) # Drop on full after 50 items
Parameters
capacity(?int) — Buffer capacity for each subscriber channel created viasubscribe().nilor0(the default) creates unbounded channels. When bounded, slow subscribers whose buffers are full will drop missed broadcasts rather than blocking the publisher.
Broadcast.subscribe()
isolate.Broadcast.subscribe()
Registers a new subscriber to this broadcast hub and returns a dedicated
Channel for receiving published messages.
Subscribers only receive messages sent after the moment of subscription.
var sub_ch = hub.subscribe()
isolate.spawn(isolate_func, sub_ch)
Returns — Channel: A channel delivering published broadcasts.
Raises IsolateError if the broadcast hub has already been closed.
Broadcast.unsubscribe()
isolate.Broadcast.unsubscribe(channel)
Unregisters a subscriber channel from this broadcast hub.
The channel immediately stops receiving new broadcasts. Any messages
already buffered in the channel remain readable via recv().
If channel was not subscribed or was already removed, this call is a
no-op.
hub.unsubscribe(sub_ch)
Parameters
channel(Channel) — The subscriber channel returned bysubscribe().
Broadcast.send()
isolate.Broadcast.send(value)
Delivers value to all currently registered subscribers.
If a subscriber’s channel has a bounded capacity and is currently full, the message is dropped for that specific subscriber without blocking the sender or delaying other subscribers.
Any subscriber channels discovered to be closed are automatically pruned.
hub.send("alert: system update")
Parameters
value(any) — The value or object graph to broadcast.
Raises IsolateError if the broadcast hub has been closed.
Broadcast.close()
isolate.Broadcast.close()
Closes the broadcast hub and all active subscriber channels.
Subscribed channels can continue draining any remaining buffered
messages. Once drained, subsequent calls to recv() on subscriber
channels will return nil. Calling send() or subscribe() after
closing raises IsolateError.
hub.close()
Broadcast.is_closed()
isolate.Broadcast.is_closed()
Checks whether the broadcast hub has been closed.
if !hub.is_closed() {
hub.send("active")
}
Returns — bool: true if closed, false otherwise.
Broadcast.subscriber_count()
isolate.Broadcast.subscriber_count()
Returns the current number of active subscribers registered to this hub.
echo "Subscribers listening: ${hub.subscriber_count()}"
Returns — int: Count of registered subscriber channels.
2026, Richard Ore and Zuri contributors
isolate.channel
import isolate.channel
isolatelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledisolate.channel.*needsimport isolate.channel.
Thread-safe, multi-producer multi-consumer (MPMC) communication queues for passing values and messages across isolate isolates.
Overview
Channels provide synchronized, data-race-free message passing between
concurrent isolate isolates. Because Zuri isolates execute within
isolated heaps, sending a value over a Channel deep-copies the object
graph across the boundary barrier, preserving internal references and
graph topologies without sharing mutable memory.
Channel handles are re-exported by the isolate module, making them
accessible directly via isolate.Channel or isolate.channel().
Channel Types & Backpressure
- Unbounded Channels (
capacity: 0ornil): Messages are enqueued without blockingsend(). The queue dynamically grows as needed. - Bounded Channels (capacity: n > 0): The channel holds at mostnunread messages. Callingsend()when the queue is full blocks the sending isolate until a consumer callsrecv(), providing automatic backpressure to prevent producer tasks from exhausting system memory.
Channel Lifecycle
- Sending:
Channel.send(val, timeout)pushes a value into the channel. If the channel is bounded and full, the call blocks until capacity is available ortimeoutelapses. - Receiving:Channel.recv(timeout)blocks until a value arrives. If the channel is closed and all queued items have been drained,recv()returnsnil. - Non-blocking Peek:Channel.try_recv()retrieves the next value immediately without blocking, returningnilif the channel is currently empty. - Closing:Channel.close()shuts down the channel for subsequent sends. Messages already in the queue remain readable until drained. Subsequent calls tosend()on a closed channel raise aIsolateError.
Multiplexing with select()
The select() function allows an isolate to wait on multiple channels
simultaneously. It blocks until at least one channel is ready to deliver
a value (or is closed), returning a pair [ready_channel, value].
Examples
1. Producer-Consumer Pipeline
import isolate
var ch = isolate.channel(10) # bounded channel with capacity 10
def producer(out_ch) {
iter var i = 1; i <= 5; i++ {
out_ch.send("item-${i}")
}
out_ch.close()
}
var producer_task = isolate.spawn(producer, ch)
while true {
var msg = ch.recv()
if msg == nil {
break # the channel is closed and empty
}
echo "Received: ${msg}"
}
producer_task.join()
2. Fan-Out / Fan-In Isolate Pool
import isolate
var results = isolate.channel()
def isolate_job(isolate_id, count, out_ch) {
iter var i = 1; i <= count; i++ {
out_ch.send("isolate-${isolate_id}-result-${i}")
}
}
var w1 = isolate.spawn(isolate_job, 1, 3, results)
var w2 = isolate.spawn(isolate_job, 2, 3, results)
iter var i = 0; i < 6; i++ {
echo results.recv()
}
w1.join()
w2.join()
3. Multiplexing Channels with select()
import isolate
var ch1 = isolate.channel()
var ch2 = isolate.channel()
isolate.spawn(@(c) { c.send("from ch1") }, ch1)
isolate.spawn(@(c) { c.send("from ch2") }, ch2)
iter var i = 0; i < 2; i++ {
var result = isolate.select([ch1, ch2], 1.0)
var ready_ch = result[0]
var value = result[1]
echo "Got: ${value}"
}
Functions
channel()
isolate.channel(capacity: ?int) -> Channel
Creates a new thread-safe Channel.
import isolate
var unbounded = isolate.channel()
var bounded = isolate.channel(50)
Parameters
capacity(?int) — Maximum buffered messages.nilor0means unbounded.
Returns Channel
select()
isolate.select(channels: list, timeout: ?number)
Multiplexes across multiple channels, blocking until at least one channel is ready to deliver a value or is closed.
The ready value is consumed from the selected channel exactly as if
recv() were called on it directly.
If multiple channels are ready simultaneously, ties are resolved in
favor of the channel with the lower index in channels.
import isolate
var ch1 = isolate.channel()
var ch2 = isolate.channel()
var res = isolate.select([ch1, ch2], 1.5)
var active_ch = res[0]
var msg = res[1]
echo "Received ${msg} from channel"
Parameters
channels(list) — A non-empty list ofChannelinstances.timeout(?number) — Maximum seconds to wait before timing out. Waits indefinitely if omitted ornil.
Returns — list: A two-element list [ready_channel, value], where
value is the message received (nil if the channel was closed and
drained).
Raises ValueError if channels is empty.
Raises IsolateTimeoutError if timeout seconds elapse before any
channel is ready.
Raises IsolateCancelledError if the calling isolate is cancelled
while blocked.
Classes
Channel
class isolate.Channel
A thread-safe, multi-producer multi-consumer (MPMC) message queue.
Constructor
isolate.Channel(capacity: ?int)
Constructs a new Channel with the specified capacity limit.
import isolate
var unbounded = isolate.channel()
var bounded = isolate.channel(100)
Parameters
capacity(?int) — Maximum number of buffered items beforesend()blocks for backpressure.nilor0creates an unbounded channel.
Raises ValueError if capacity is negative.
Channel.send()
isolate.Channel.send(value, timeout: ?number)
Sends value into the channel.
If the channel is bounded and currently full, this call blocks the calling isolate until space becomes available, the timeout elapses, or the caller is cancelled.
ch.send("data payload")
ch.send(42, 0.5) # wait at most 500ms
Parameters
value(any) — The value or object graph to send.timeout(?number) — Maximum seconds to wait before timing out. Waits indefinitely if omitted ornil.
Raises IsolateError if the channel has been closed.
Raises IsolateTimeoutError if timeout seconds elapse before
space is available.
Raises IsolateCancelledError if the calling isolate is cancelled
while blocked.
Channel.recv()
isolate.Channel.recv(timeout: ?number)
Blocks until a message is available and retrieves it.
If the channel is closed and all buffered messages have been received,
recv() returns nil.
var item = ch.recv()
var timed_item = ch.recv(2.0)
Parameters
timeout(?number) — Maximum seconds to wait for a message. Waits indefinitely if omitted ornil.
Returns — any: The received value, or nil if the channel is closed
and empty.
Raises IsolateTimeoutError if timeout seconds elapse before a
value is received.
Raises IsolateCancelledError if the calling isolate is cancelled
while blocked.
Channel.try_recv()
isolate.Channel.try_recv()
Attempts to receive a value without blocking.
var item = ch.try_recv()
if item != nil {
echo "Processed: ${item}"
}
Returns — any: The next available message, or nil if the channel
is currently empty or closed.
Channel.close()
isolate.Channel.close()
Closes the channel.
Buffered messages remain available for consumers to read via recv() or
try_recv(). Calling send() after closing raises IsolateError.
ch.close()
Channel.is_closed()
isolate.Channel.is_closed()
Checks whether the channel has been closed.
if !ch.is_closed() {
ch.send("active")
}
Returns — bool: true if the channel is closed, false otherwise.
Channel.length()
isolate.Channel.length()
Returns the current number of buffered, unread messages in the channel.
var pending = ch.length()
Returns — int: Number of queued items.
Channel.handle()
isolate.Channel.handle()
Returns the underlying native channel pointer.
Used internally by select(). Not intended for direct application
usage.
2026, Richard Ore and Zuri contributors
isolate.error
import isolate.error
isolatelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledisolate.error.*needsimport isolate.error.
IsolateError, kept in its own leaf module (no imports of its own) so
both index and channel can depend on it without a circular import
between them. Re-exported by isolate.
Classes
IsolateError
class isolate.IsolateError < Error
Raised when an isolate’s function raises an uncaught error, a Channel
operation fails, or a value cannot cross an isolate boundary.
Constructor
isolate.IsolateError(message)
IsolateTimeoutError
class isolate.IsolateTimeoutError < IsolateError
Raised when Isolate.join(), Channel.send(), or Channel.recv() is
given a timeout and it elapses before the operation completes.
Constructor
isolate.IsolateTimeoutError(message)
IsolateCancelledError
class isolate.IsolateCancelledError < IsolateError
Raised from Isolate.join(), Channel.send(), Channel.recv(),
isolate.wait_any(), isolate.wait_all(), or isolate.select() when
the CALLING isolate; the one blocked in that call, not whatever it was
waiting on; is cancel()ed while still waiting.
Constructor
isolate.IsolateCancelledError(message)
imagine
import imagine
Reading, writing, drawing and transforming raster images.
imagine covers the whole path an image takes through a program:
decoding it from a file or an upload, resizing and cropping it,
adjusting its colour, drawing shapes and text onto it, compositing
several together, and writing the result back out in whatever format
suits.
import imagine { Image, Font, LANCZOS }
Image.open('photo.jpg')
.thumbnail(600, 600, LANCZOS)
.sharpen(0.6)
.save('thumb.webp', { quality: 82 })
The shape of the API
Almost everything is a method on [[imagine.Image]], and almost everything returns an image, so operations chain. There is one rule worth learning up front:
- Operations that change the image’s size return a new image.
resize(),crop(),rotate(),flip()andpad()leave the original alone. - Everything else changes the image in place. Drawing, filters and compositing modify the image you called them on and hand it back for chaining.
Call clone() when you want to keep an image before filtering it.
Colours
Anywhere a colour is expected you can write a [[imagine.Color]], a
hexadecimal string, a CSS colour name, a packed 0xRRGGBBAA number, or
a list of channels. These are all the same red:
image.fill(Color(255, 0, 0))
image.fill('#ff0000')
image.fill('red')
image.fill(0xFF0000FF)
image.fill([255, 0, 0])
Alpha runs 0 (transparent) to 255 (opaque), as in CSS and PNG. The colour space conversions come from [[colors]], so hue is in degrees and saturation and lightness are in percentage points.
Pixels
An image is 8-bit RGBA with straight (not premultiplied) alpha, laid out
row by row with no padding, so pixel (x, y) begins at byte (y * width + x) * 4.
[[imagine.Canvas.pixels]] hands back that buffer live, which is the
fastest way to write an operation this module does not provide:
var buffer = image.pixels()
var total = buffer.length()
iter var at = 0; at < total; at += 4 {
buffer[at] = 255 - buffer[at]
}
Formats
Read: PNG, JPEG, GIF, BMP, TIFF, TGA, WebP, QOI, ICO, PNM, WBMP. Write: all of those plus AVIF.
AVIF is write-only, and TGA carries no signature of its own so it can only be identified by its file extension or by naming the format outright. [[imagine.capabilities]] reports what this build can actually do, so a program can check rather than guess.
Animated GIF and animated WebP are read through [[imagine.Animation]], and animated GIF can be written.
Drawing
Every filled shape becomes a polygon and goes through one anti-aliased scanline rasterizer, and every outline becomes the polygon around its stroke and goes through the same one. Shapes this module does not name can be drawn by handing [[imagine.Canvas.fill_polygon]] the points.
import imagine { Image, Color }
Image(300, 200, 'white')
.fill_rounded_rect(20, 20, 260, 160, 16, '#4f46e5')
.circle(150, 100, 50, 'white', { thickness: 4 })
.save('card.png')
Text
Text needs a font, loaded from a file with [[imagine.Font.load]] or
found on the machine with [[imagine.Font.system]]. Glyphs are placed by
advance width with kerning; complex scripts that need shaping are not
supported, and the Font documentation says exactly what that rules
out.
Memory
Four bytes per pixel, so a 6000x4000 photograph occupies 96 MB however small its file was. For images whose size you do not control, [[imagine.probe]] reads the dimensions out of the header without decoding anything.
var header = imagine.probe(upload)
if header == nil or header.width * header.height > 40000000 {
raise Exception('image is missing or too large')
}
The imagine API
Every public name in imagine, wherever it is declared. Each links to
the page that documents it.
| Name | Kind | Summary |
|---|---|---|
imagine.ALIGN_CENTER | constant | Lines of a multi-line string are centred against each other. |
imagine.ALIGN_LEFT | constant | Lines of a multi-line string start at the same left edge. |
imagine.ALIGN_RIGHT | constant | Lines of a multi-line string end at the same right edge. |
imagine.AVIF | constant | AVIF. |
imagine.Animation | class | A sequence of images with a delay between each, read from or written to an animated format. |
imagine.BICUBIC | constant | Bicubic (Catmull-Rom). |
imagine.BILINEAR | constant | Bilinear. |
imagine.BLACK | constant | Opaque black. |
imagine.BLEND_ADD | constant | Adds the channels, clamped at white. |
imagine.BLEND_COLOR_BURN | constant | Darkens the backdrop toward the source. |
imagine.BLEND_COLOR_DODGE | constant | Brightens the backdrop toward the source. |
imagine.BLEND_DARKEN | constant | Keeps whichever channel is darker. |
imagine.BLEND_DIFFERENCE | constant | The absolute difference between the two colours. |
imagine.BLEND_EXCLUSION | constant | Like difference, but lower contrast in the midtones. |
imagine.BLEND_HARD_LIGHT | constant | Overlay with the roles of source and backdrop swapped. |
imagine.BLEND_LIGHTEN | constant | Keeps whichever channel is lighter. |
imagine.BLEND_MULTIPLY | constant | Multiplies the two colours. |
imagine.BLEND_NORMAL | constant | Ordinary alpha compositing: the source is painted over the destination according to its alpha. |
imagine.BLEND_OVERLAY | constant | Multiply where the backdrop is dark, screen where it is light. |
imagine.BLEND_SCREEN | constant | The inverse of multiply. |
imagine.BLEND_SOFT_LIGHT | constant | A gentler hard light, as if the source were a diffuse spotlight. |
imagine.BLEND_SUBTRACT | constant | Subtracts the source from the backdrop, clamped at black. |
imagine.BLUE | constant | Opaque pure blue. |
imagine.BMP | constant | BMP. |
imagine.BOTTOM | constant | |
imagine.BOTTOM_LEFT | constant | |
imagine.BOTTOM_RIGHT | constant | |
imagine.BoundsError | class | Raised when a rectangle, crop or resize falls outside the image, or when a dimension is zero or negative. |
imagine.CAP_BUTT | constant | The stroke stops dead at its endpoint. |
imagine.CAP_ROUND | constant | The stroke ends in a half-disc, so it reaches half its width past the endpoint. |
imagine.CAP_SQUARE | constant | The stroke ends in a square, so it reaches half its width past the endpoint, the same distance a round cap… |
imagine.CENTER | constant | |
imagine.CYAN | constant | Opaque cyan. |
imagine.Canvas | class | A mutable RGBA pixel surface and everything that draws onto one. |
imagine.Color | class | An 8-bit RGBA colour. |
imagine.DecodeError | class | Raised when image data cannot be read: the bytes are not an image at all, the format is one this build cannot… |
imagine.EDGE_CLAMP | constant | Pixels off the edge take the value of the nearest edge pixel. |
imagine.EDGE_TRANSPARENT | constant | Pixels off the edge are treated as transparent black. |
imagine.EDGE_WRAP | constant | Pixels off the edge wrap to the opposite side. |
imagine.EncodeError | class | Raised when an image cannot be written in the requested format, either because this build has no encoder for… |
imagine.FLIP_BOTH | constant | Mirror both ways at once, which is the same as rotating 180 degrees. |
imagine.FLIP_HORIZONTAL | constant | Mirror left to right. |
imagine.FLIP_VERTICAL | constant | Mirror top to bottom. |
imagine.Font | class | A typeface at a particular size, ready to draw with. |
imagine.FontError | class | Raised when a font cannot be parsed, cannot be found on the system, or does not carry the horizontal metrics… |
imagine.FormatError | class | Raised when a format name is not one imagine knows, or when a file extension cannot be mapped to a format. |
imagine.GAUSSIAN | constant | Gaussian. |
imagine.GIF | constant | GIF. |
imagine.GRAY | constant | Opaque mid grey. |
imagine.GREEN | constant | Opaque pure green. |
imagine.ICO | constant | Windows ICO. |
imagine.Image | class | A raster image: a rectangle of 8-bit RGBA pixels, everything that draws onto one, and everything that… |
imagine.ImageError | class | Base class for every error the imagine module raises. |
imagine.JPEG | constant | JPEG. |
imagine.LANCZOS | constant | Lanczos with a 3-lobe window. |
imagine.LEFT | constant | |
imagine.MAGENTA | constant | Opaque magenta. |
imagine.NEAREST | constant | Nearest-neighbour. |
imagine.PNG | constant | PNG. |
imagine.PNM | constant | Netpbm (PBM, PGM, PPM). |
imagine.QOI | constant | QOI, the Quite OK Image format. |
imagine.RED | constant | Opaque pure red. |
imagine.RIGHT | constant | |
imagine.StrokeFont | class | The built-in font: a sans-serif drawn from centre-line strokes rather than loaded from a file. |
imagine.TGA | constant | Truevision TGA. |
imagine.TIFF | constant | TIFF. |
imagine.TOP | constant | |
imagine.TOP_LEFT | constant | Where a smaller image or a piece of text sits inside a larger box, used by Image.cover(), Image.contain()… |
imagine.TOP_RIGHT | constant | |
imagine.TRANSPARENT | constant | |
imagine.WBMP | constant | Wireless Bitmap. |
imagine.WEBP | constant | WebP. |
imagine.WHITE | constant | Opaque white. |
imagine.YELLOW | constant | Opaque yellow. |
imagine.capabilities | function | What this build can do, as a dictionary holding decode, encode and animated, each a list of format… |
imagine.create | function | Creates a new image. |
imagine.decode | function | Decodes image data held in memory. |
imagine.detect | function | Identifies image data by its contents, returning a format name or nil. |
imagine.filters.LUMA | constant | Rec. |
imagine.filters.MATRIX_LUMA | constant | The luma weights the colour-matrix filters use, from the SVG and CSS filter specifications. |
imagine.filters.box_blur_kernel | function | A 3x3 box blur kernel; every neighbour counts equally. |
imagine.filters.brightness_lut | function | A lookup table that adds a fixed amount to every value. |
imagine.filters.build_lut | function | Builds a 256-entry lookup table from a function. |
imagine.filters.combine_matrices | function | Multiplies two colour matrices, giving one matrix with the effect of applying first and then second. |
imagine.filters.contrast_lut | function | A lookup table that pushes values away from or toward mid-grey. |
imagine.filters.duotone_matrix | function | A colour matrix that replaces every pixel’s colour with a blend between two colours chosen by its brightness,… |
imagine.filters.edge_kernel | function | A 3x3 Laplacian kernel that leaves only the edges. |
imagine.filters.emboss_kernel | function | A 3x3 kernel that lifts edges into a grey relief. |
imagine.filters.gamma_lut | function | A lookup table applying a gamma curve. |
imagine.filters.gaussian_kernel | function | A square Gaussian kernel with the given standard deviation. |
imagine.filters.grayscale_matrix | function | A colour matrix that collapses every colour to its grey of equal perceived brightness. |
imagine.filters.hue_rotate_matrix | function | A colour matrix that rotates every hue around the colour wheel by an angle in degrees, leaving brightness and… |
imagine.filters.identity_lut | function | A lookup table that leaves every value alone. |
imagine.filters.identity_matrix | function | The identity colour matrix: applying it changes nothing. |
imagine.filters.invert_lut | function | A lookup table that inverts every value. |
imagine.filters.levels_lut | function | A lookup table implementing a levels adjustment. |
imagine.filters.mean_removal_kernel | function | A 3x3 kernel that removes local mean, exaggerating detail. |
imagine.filters.posterize_lut | function | A lookup table that reduces each channel to a fixed number of evenly spaced steps. |
imagine.filters.saturation_matrix | function | A colour matrix that scales saturation. |
imagine.filters.scale_lut | function | A lookup table that scales every value by a factor. |
imagine.filters.sepia_matrix | function | A colour matrix approximating the warm brown cast of a sepia photograph. |
imagine.filters.sharpen_kernel | function | A 3x3 sharpening kernel. |
imagine.filters.smooth_kernel | function | A 3x3 smoothing kernel weighted toward the centre pixel. |
imagine.filters.threshold_lut | function | A lookup table that forces every value to black or white. |
imagine.formats.animatable | function | Every format this build can read as an animation. |
imagine.formats.decode_wbmp | function | Decodes a WBMP into straight RGBA pixels. |
imagine.formats.detect | function | Identifies image data by its content rather than its name, or returns nil when the bytes are not a… |
imagine.formats.encode_wbmp | function | Encodes straight RGBA pixels as a WBMP. |
imagine.formats.extension_for | function | The file extension a format is normally written with, without a leading dot. |
imagine.formats.from_extension | function | The format name a file extension implies, or nil when the extension is not one this module knows. |
imagine.formats.mime_for | function | The IANA media type for a format, suitable for a Content-Type header. |
imagine.formats.normalize | function | Normalises a format name, accepting the common aliases. |
imagine.formats.probe | function | Reads an image’s format and dimensions without decoding its pixels. |
imagine.formats.readable | function | Every format this build can read. |
imagine.formats.writable | function | Every format this build can write. |
imagine.open | function | Opens an image file. |
imagine.probe | function | Reads an image’s format and dimensions from its header, without decoding any pixels. |
imagine.strokefont.ASCENDER | constant | How far the tallest glyphs rise above the baseline. |
imagine.strokefont.CAP_HEIGHT | constant | The height of a capital letter. |
imagine.strokefont.DESCENDER | constant | How far descenders fall below the baseline, as a negative number. |
imagine.strokefont.EM | constant | The em square’s height in design units. |
imagine.strokefont.GLYPHS | constant | Every glyph, keyed by character. |
imagine.strokefont.NOTDEF | constant | What an unmapped character draws: an empty box, the same convention a font uses for a glyph it does not have. |
imagine.strokefont.WEIGHT | constant | The default stroke width in design units, a little under a tenth of the em, which is the usual weight for a… |
imagine.strokefont.X_HEIGHT | constant | The height of a lowercase letter with no ascender. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
imagine.animation | imagine.animation.* | Animation: an ordered set of frames with per-frame delays, which is what an animated GIF or WebP decodes to… |
imagine.canvas | imagine.canvas.* | Canvas: the drawing surface. |
imagine.color | imagine.* | Color: one RGBA colour, and the conversions between the ways of naming one. |
imagine.constants | imagine.* | Every named constant the module takes as an argument: resampling filters, blend modes, line caps, edge… |
imagine.errors | imagine.* | Every error the module raises, under ImageError as their root. |
imagine.filters | imagine.filters.* | The maths behind the filters: colour matrices, lookup tables and convolution kernels. |
imagine.font | imagine.font.* | Font: a loaded TrueType or OpenType face, ready to measure and draw text with. |
imagine.formats | imagine.formats.* | What the build can actually read and write, and how to tell one format from another. |
imagine.image | imagine.image.* | Image: a raster image as an RGBA pixel buffer, and everything that transforms one. |
imagine.strokefont | imagine.strokefont.* | StrokeFont: the font built into the module, defined as stroke geometry rather than glyph outlines. |
Functions
open()
imagine.open(source, options) -> Image
Opens an image file.
A shorthand for [[imagine.Image.open]].
Parameters
source(string|file)options(?dict)
Returns Image
decode()
imagine.decode(data, options) -> Image
Decodes image data held in memory.
A shorthand for [[imagine.Image.decode]].
Parameters
data(bytes)options(?dict)
Returns Image
create()
imagine.create(width, height, fill) -> Image
Creates a new image.
A shorthand for the [[imagine.Image]] constructor.
Parameters
width(number)height(number)fill(?Color|string|number)
Returns Image
probe()
imagine.probe(data) -> ?dict
Reads an image’s format and dimensions from its header, without decoding any pixels.
Returns {format, width, height}, or nil when the data is not a
recognisable image. Reading a large JPEG’s header costs microseconds
where decoding it costs tens of milliseconds, which makes this the right
way to size up an upload before committing to it.
Parameters
data(bytes)
Returns ?dict
detect()
imagine.detect(data) -> ?string
Identifies image data by its contents, returning a format name or nil.
Parameters
data(bytes)
Returns ?string
capabilities()
imagine.capabilities() -> dict
What this build can do, as a dictionary holding decode, encode and
animated, each a list of format names.
Worth checking rather than assuming: AVIF appears under encode but not
decode, and which formats are compiled in can differ between builds.
Returns dict
2021, Richard Ore and Zuri contributors
imagine.animation
import imagine.animation
imaginelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledimagine.animation.*needsimport imagine.animation.
Animation: an ordered set of frames with per-frame delays, which is
what an animated GIF or WebP decodes to and encodes from.
Frames are full images rather than deltas, so each can be edited on its own and the encoder works out what actually changed.
Classes
Animation
class imagine.Animation
A sequence of images with a delay between each, read from or written to an animated format.
Frames are whole images, already composited against whatever came before them, so frame 5 can be pulled out and used on its own without replaying the first four. That is the useful shape almost all of the time, and it is why the disposal and transparency rules an animated GIF is built from never surface here.
import imagine { Animation }
var animation = Animation.open('loading.gif')
echo '${animation.length()} frames, ${animation.duration()}ms total'
animation
.map(@(frame) {
return frame.grayscale()
})
.save('loading-grey.gif')
What can be read and written
Animated GIF and animated WebP can both be read. Only GIF can be
written; an animation saved as WebP raises EncodeError rather than
quietly writing one frame. [[imagine.capabilities]] lists what is
available under animated.
Memory
Every frame is a full image, so a 100-frame 500x500 animation is 100 MB
decoded. Animations are worth streaming frame by frame when they are
large, which frames() allows by handing back the list itself.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
imagine.Animation(frames: list, delay, repeat, format)
Creates an animation from a list of images.
Every frame must be the same size, since animated formats have one canvas that each frame paints into.
Parameters
frames(list) — A list of [[imagine.Image]].delay(?number|list) — Milliseconds per frame, either one number for all of them or one per frame - Default: 100.repeat(?number) — How many times to loop, 0 for forever - Default: 0.format(?string) — The format these frames were decoded from, recorded as metadata - Default: nil.
Raises ImageError When the frames are not all images of one size.
Animation.open()
imagine.Animation.open(source) -> Animation
Opens an animated image file.
A still image opens as a one-frame animation rather than an error, so code that handles both does not need to branch.
Parameters
source(string|file)
Returns Animation
Raises DecodeError When the file is not a readable image.
Animation.decode()
imagine.Animation.decode(data: bytes) -> Animation
Decodes animated image data held in memory.
Parameters
data(bytes)
Returns Animation
Raises DecodeError When the data is not a readable image.
Animation.length()
imagine.Animation.length() -> number
How many frames the animation has.
Returns number
Animation.frames()
imagine.Animation.frames() -> list
The frames, as a list of [[imagine.Image]].
This is the live list, not a copy: changing an image in it changes the
animation, which is what makes frame-by-frame editing possible without
duplicating everything. Use map() when you want the original left
alone.
Returns list
Animation.frame()
imagine.Animation.frame(index: number) -> Image
One frame by index, counting from zero.
Parameters
index(number)
Returns Image
Raises ImageError When there is no such frame.
Animation.delays()
imagine.Animation.delays() -> list
The per-frame delays in milliseconds.
Returns list
Animation.duration()
imagine.Animation.duration() -> number
How long one pass through the animation takes, in milliseconds.
Returns number
Animation.width()
imagine.Animation.width() -> number
The frames’ shared width in pixels.
Returns number
Animation.height()
imagine.Animation.height() -> number
The frames’ shared height in pixels.
Returns number
Animation.format()
imagine.Animation.format() -> ?string
The format this animation was decoded from, or nil.
Returns ?string
Animation.repeat()
imagine.Animation.repeat(times: number)
Sets how many times the animation loops when written out.
Parameters
times(number) — 0 loops forever.
Returns — self
Animation.delay()
imagine.Animation.delay(delay)
Sets the delay between frames.
Parameters
delay(number|list) — One value for every frame, or one per frame.
Returns — self
Animation.map()
imagine.Animation.map(fn: function) -> Animation
Applies a function to every frame, returning a new animation.
Each frame is copied before the function sees it, so a filter chain
works directly as the body and this animation is left alone. That copy
is the reason map() costs as much memory again as the animation
itself; to filter in place instead, walk frames() and change each
image directly.
var grey = animation.map(@(frame) {
return frame.grayscale().blur(1)
})
Parameters
fn(function(1))
Returns Animation
Raises ImageError When the function returns anything but an image.
Animation.reverse()
imagine.Animation.reverse() -> Animation
Returns a new animation with the frames in reverse order.
The frames themselves are shared with this animation rather than copied,
since reordering them changes nothing about them. Editing one afterwards
therefore shows up in both; map() is the one that copies.
Returns Animation
Animation.to_image()
imagine.Animation.to_image() -> Image
Returns the animation as a single image: its first frame.
Returns Image
Animation.encode()
imagine.Animation.encode(format, options) -> bytes
Encodes the animation and returns the bytes.
Parameters
format(?string) — Default:GIF.options(?dict)
Returns bytes
Raises EncodeError When the format cannot hold an animation.
Animation.save()
imagine.Animation.save(path: string, options)
Writes the animation to a file.
Parameters
path(string)options(?dict) —format, plus everythingencode()takes.
Returns — self
Animation.to_string()
imagine.Animation.to_string()
2026, Richard Ore and Zuri contributors
imagine.canvas
import imagine.canvas
imaginelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledimagine.canvas.*needsimport imagine.canvas.
Canvas: the drawing surface. Every shape the module can draw is a
method on it.
A canvas wraps an Image’s pixel buffer rather than copying it, so
drawing goes straight into the image being built. The shape methods
share three primitives underneath (span fill, blend, and stroke), which
is why a new shape rarely needs new native code.
Classes
Canvas
class imagine.Canvas
A mutable RGBA pixel surface and everything that draws onto one.
Canvas is the lower half of Image: it owns the pixel buffer and
knows how to put colour into it, but nothing about files, formats or
resizing. It is separate mostly so the drawing code can be read on its
own, and it is rarely worth using directly; Image inherits all of
this.
How drawing works here
Every filled shape becomes a polygon and goes through one scanline
rasterizer, and every stroked shape becomes the polygon outlining its
stroke and goes through the same one. That means anti-aliasing, clipping
and the fill rule are implemented once rather than once per primitive,
and a shape this module does not offer can be drawn by handing
fill_polygon() the points for it.
Coordinates
The origin is the top-left corner. Integer coordinates fall on pixel corners, not centres, so a rectangle from (0, 0) to (10, 10) covers exactly the first ten pixels in each direction with no half-covered edge. Fractional coordinates are meaningful and are what anti-aliasing acts on.
Drawing outside the surface is not an error. Anything that falls outside is clipped away, which is what makes it safe to draw a shape that only partly overlaps the image.
Constructor
imagine.Canvas(pixels: bytes, width: number, height: number)
Wraps an existing pixel buffer.
The buffer is taken as-is and not copied, so the caller must not keep
writing to it independently. Image is the supported way to get a
Canvas; this constructor exists for code that already has pixels from
somewhere else.
Parameters
pixels(bytes) —width * height * 4bytes of RGBA.width(number)height(number)
Canvas.width()
imagine.Canvas.width() -> number
The width in pixels.
Returns number
Canvas.height()
imagine.Canvas.height() -> number
The height in pixels.
Returns number
Canvas.pixels()
imagine.Canvas.pixels() -> bytes
The raw RGBA pixel buffer.
This is the live buffer, not a copy: writing to it changes the image.
Pixel (x, y) starts at byte (y * width + x) * 4 and runs red, green,
blue, alpha, with alpha straight rather than premultiplied.
Handing this out is deliberate. Reading and writing bytes directly is far faster than a method call per pixel, so an operation this module does not provide can still be written efficiently in Zuri.
Returns bytes
Canvas.set_pixels()
imagine.Canvas.set_pixels(pixels: bytes)
Replaces the surface’s pixels with another buffer.
The buffer must hold exactly width * height * 4 bytes, and is adopted
rather than copied, so whatever produced it must not keep writing to it
afterwards.
The pair to pixels(): that one hands the buffer out so an operation
this module does not provide can be written directly, and this one takes
a buffer back. Saving a copy before a destructive filter and restoring
it afterwards is the usual reason to want it.
Parameters
pixels(bytes)
Returns — self
Raises ImageError When the buffer is the wrong size.
Canvas.clip()
imagine.Canvas.clip(x, y, width, height)
Restricts every subsequent drawing operation to a rectangle.
The rectangle is intersected with the surface, so a clip larger than the
image is the same as no clip. Passing no arguments, or calling
clear_clip(), removes it.
Parameters
x(number)y(number)width(number)height(number)
Returns — self
Canvas.clear_clip()
imagine.Canvas.clear_clip()
Removes any clipping rectangle.
Returns — self
Canvas.get_clip()
imagine.Canvas.get_clip() -> ?dict
The current clipping rectangle as {x, y, width, height}, or nil when
there isn’t one.
Returns ?dict
Canvas.antialias()
imagine.Canvas.antialias(enabled)
Turns anti-aliasing on or off for subsequent drawing.
On by default. Turning it off makes every edge hard, which is faster and is what you want for pixel-exact output such as barcodes, or when drawing into a mask that will be thresholded anyway.
Parameters
enabled(bool)
Returns — self
Canvas.thickness()
imagine.Canvas.thickness(thickness: number)
Sets the stroke width in pixels for lines and outlines.
Strokes are centred on the path, so a thickness of 4 puts 2 pixels on each side. The default is 1.
Parameters
thickness(number)
Returns — self
Canvas.cap()
imagine.Canvas.cap(style)
Sets how the two ends of an open stroke are finished.
CAP_ROUND (the default) ends in a half-disc, CAP_SQUARE in a square,
and both reach half the stroke’s width past the endpoint. CAP_BUTT
stops exactly at it.
Reach for CAP_BUTT when the coordinates have to mean exactly what they
say: segments meeting end to end, a scale bar of a known length, the
pieces of a dashed line. The joins between a path’s segments are always
round and are not affected by this.
chart.cap(CAP_BUTT).thickness(6)
chart.line(40, 200, 40, 40, '#334155')
Parameters
style(string) —CAP_BUTT,CAP_ROUNDorCAP_SQUARE.
Returns — self
Canvas.get_cap()
imagine.Canvas.get_cap() -> string
The line cap in force.
Returns string
Canvas.get_pixel()
imagine.Canvas.get_pixel(x, y) -> ?Color
Returns the colour at a pixel, or nil when the coordinates fall
outside the image.
Clipping does not apply; a clip restricts what is written, not what can be read.
Parameters
x(number)y(number)
Returns ?Color
Canvas.set_pixel()
imagine.Canvas.set_pixel(x, y, color)
Replaces the colour at a pixel, ignoring whatever was there.
This overwrites rather than blends, so drawing a half-transparent colour
leaves a half-transparent pixel instead of compositing it over the old
one. Use blend_pixel() for the compositing version, which is what
every other drawing method uses.
Coordinates outside the image or outside the clip are ignored.
Parameters
x(number)y(number)color(Color|string|number)
Returns — self
Canvas.pixel()
imagine.Canvas.pixel(x, y, color)
Draws one pixel, compositing it over whatever is already there.
The single-pixel member of the drawing family, alongside line() and
rect(). It is the same operation as blend_pixel(), under the name
that reads correctly in a chain of drawing calls.
Coordinates outside the image or outside the clip are ignored.
Parameters
x(number)y(number)color(Color|string|number)
Returns — self
Canvas.blend_pixel()
imagine.Canvas.blend_pixel(x, y, color)
Composites a colour over the pixel already there, the way every drawing operation does.
Coordinates outside the image or outside the clip are ignored.
Parameters
x(number)y(number)color(Color|string|number)
Returns — self
Canvas.fill_polygon()
imagine.Canvas.fill_polygon(points, color, options)
Fills a polygon.
The points are a flat list of alternating x and y values, or a list of
[x, y] pairs; both are accepted because both read naturally depending
on where the points came from. The outline is closed automatically, so
the last point does not need to repeat the first.
Self-intersecting outlines are filled by the non-zero winding rule by
default, which is what fills a five-pointed star solid. Pass { even_odd: true }
for the other convention, which leaves the middle of that star empty.
Parameters
points(list)color(Color|string|number)options(?dict) —even_odd(bool).
Returns — self
Canvas.polygon()
imagine.Canvas.polygon(points, color, options)
Draws a polygon’s outline, closing it back to the first point.
Parameters
points(list)color(Color|string|number)options(?dict) —thickness.
Returns — self
Canvas.polyline()
imagine.Canvas.polyline(points, color, options)
Draws a connected run of line segments without closing it.
Parameters
points(list)color(Color|string|number)options(?dict) —thickness.
Returns — self
Canvas.line()
imagine.Canvas.line(x1, y1, x2, y2, color, options)
Draws a straight line between two points.
The line is thickness() pixels wide, centred on the path, and
anti-aliased unless that has been turned off.
Parameters
x1(number)y1(number)x2(number)y2(number)color(Color|string|number)options(?dict) —thickness.
Returns — self
Canvas.rect()
imagine.Canvas.rect(x, y, width, height, color, options)
Draws the outline of a rectangle.
The stroke is centred on the rectangle’s edge, so half of a thick outline falls inside it and half outside.
Parameters
x(number)y(number)width(number)height(number)color(Color|string|number)options(?dict) —thickness.
Returns — self
Canvas.fill_rect()
imagine.Canvas.fill_rect(x, y, width, height, color)
Fills a rectangle.
Parameters
x(number)y(number)width(number)height(number)color(Color|string|number)
Returns — self
Canvas.rounded_rect()
imagine.Canvas.rounded_rect(x, y, width, height, radius, color, options)
Draws the outline of a rectangle with rounded corners.
A radius larger than half the shorter side is reduced to fit, so a very large radius gives a stadium shape rather than a broken one.
Parameters
x(number)y(number)width(number)height(number)radius(number)color(Color|string|number)options(?dict) —thickness.
Returns — self
Canvas.fill_rounded_rect()
imagine.Canvas.fill_rounded_rect(x, y, width, height, radius, color)
Fills a rectangle with rounded corners.
Parameters
x(number)y(number)width(number)height(number)radius(number)color(Color|string|number)
Returns — self
Canvas.circle()
imagine.Canvas.circle(cx, cy, radius, color, options)
Draws the outline of a circle.
Parameters
cx(number) — The centre’s x coordinate.cy(number) — The centre’s y coordinate.radius(number)color(Color|string|number)options(?dict) —thickness.
Returns — self
Canvas.fill_circle()
imagine.Canvas.fill_circle(cx, cy, radius, color)
Fills a circle.
Parameters
cx(number)cy(number)radius(number)color(Color|string|number)
Returns — self
Canvas.ellipse()
imagine.Canvas.ellipse(cx, cy, rx, ry, color, options)
Draws the outline of an ellipse.
Parameters
cx(number) — The centre’s x coordinate.cy(number) — The centre’s y coordinate.rx(number) — The horizontal radius.ry(number) — The vertical radius.color(Color|string|number)options(?dict) —thickness.
Returns — self
Canvas.fill_ellipse()
imagine.Canvas.fill_ellipse(cx, cy, rx, ry, color)
Fills an ellipse.
Parameters
cx(number)cy(number)rx(number)ry(number)color(Color|string|number)
Returns — self
Canvas.arc()
imagine.Canvas.arc(cx, cy, rx, ry, start, end, color, options)
Draws an elliptical arc.
Angles are in degrees, measured clockwise from three o’clock, matching the direction the y axis runs. An end angle below the start angle sweeps the long way round.
Parameters
cx(number)cy(number)rx(number)ry(number)start(number) — The starting angle in degrees.end(number) — The ending angle in degrees.color(Color|string|number)options(?dict) —thickness.
Returns — self
Canvas.pie()
imagine.Canvas.pie(cx, cy, rx, ry, start, end, color)
Fills a pie slice: an arc closed back through the centre.
Parameters
cx(number)cy(number)rx(number)ry(number)start(number) — The starting angle in degrees.end(number) — The ending angle in degrees.color(Color|string|number)
Returns — self
Canvas.chord()
imagine.Canvas.chord(cx, cy, rx, ry, start, end, color)
Fills the region between an arc and its chord.
Parameters
cx(number)cy(number)rx(number)ry(number)start(number)end(number)color(Color|string|number)
Returns — self
Canvas.bezier()
imagine.Canvas.bezier(x1, y1, cx1, cy1, cx2, cy2, x2, y2, color, options)
Draws a cubic Bezier curve through four control points.
The curve starts at the first point, ends at the fourth, and is pulled toward the two in between without passing through them.
Parameters
x1(number)y1(number)cx1(number) — The first control point’s x coordinate.cy1(number)cx2(number) — The second control point’s x coordinate.cy2(number)x2(number)y2(number)color(Color|string|number)options(?dict) —thickness.
Returns — self
Canvas.fill()
imagine.Canvas.fill(color)
Paints every pixel of the surface, ignoring the clip and whatever was there before.
This is a replace, not a blend: filling with a transparent colour empties the image rather than leaving it unchanged.
Parameters
color(Color|string|number)
Returns — self
Canvas.clear()
imagine.Canvas.clear()
Clears the surface to fully transparent.
Returns — self
Canvas.flood_fill()
imagine.Canvas.flood_fill(x, y, color, tolerance)
Flood fills the connected region of similar colour containing (x, y).
Similarity is measured against the colour that was at the starting
point, as the largest difference across the four channels. tolerance
is that difference in 0-255 units, so 0 spreads only through exactly
equal pixels and 255 fills everything.
Filling is four-connected: it spreads up, down, left and right, but not diagonally, so a one-pixel diagonal line holds it back. The fill is never anti-aliased.
Parameters
x(number)y(number)color(Color|string|number)tolerance(?number) — Default: 0.
Returns — self
Canvas.linear_gradient()
imagine.Canvas.linear_gradient(x1, y1, x2, y2, stops, options)
Fills a rectangle with a linear gradient running between two points.
The gradient’s direction and length are the vector from (x1, y1) to
(x2, y2). Everything before the first point takes the first stop’s
colour and everything past the second takes the last stop’s, so a short
vector across a large area gives a hard transition with flat bands on
either side.
# top to bottom, dark to light
image.linear_gradient(0, 0, 0, image.height(), ['#0f172a', '#334155'])
Parameters
x1(number)y1(number)x2(number)y2(number)stops(list) — Colours, or[offset, colour]pairs with the offset from 0 to 1.options(?dict) —rect(a{x, y, width, height}to confine the fill to, default the whole surface),blend(bool, default false: composite over what is there rather than replacing it).
Returns — self
Canvas.radial_gradient()
imagine.Canvas.radial_gradient(cx, cy, radius, stops, options)
Fills a rectangle with a radial gradient spreading from a centre.
# a soft vignette
image.radial_gradient(cx, cy, radius, [
[0, Color(0, 0, 0, 0)],
[1, Color(0, 0, 0, 180)],
], { blend: true })
Parameters
cx(number) — The centre’s x coordinate.cy(number) — The centre’s y coordinate.radius(number) — Where the last stop lands.stops(list) — Colours, or[offset, colour]pairs.options(?dict) —rect,blend. The same aslinear_gradient().
Returns — self
Canvas.draw_mask()
imagine.Canvas.draw_mask(mask: bytes, mask_width, mask_height, x, y, color)
Paints a colour through an 8-bit coverage mask.
The mask is one byte per pixel, 0 for nothing and 255 for full coverage, laid out row-major with no padding. Coverage scales the colour’s alpha, so this is how anti-aliased text and any externally rasterized shape reach the surface.
Parameters
mask(bytes)mask_width(number)mask_height(number)x(number) — Where the mask’s top-left corner lands.y(number)color(Color|string|number)
Returns — self
2026, Richard Ore and Zuri contributors
imagine.color
import imagine
Everything here is re-exported by
imagine, soimport imagineis enough and the names are called asimagine.*. Importingimagine.coloron its own works too and reaches the same definitions.
Color: one RGBA colour, and the conversions between the ways of naming
one.
It reads and writes hex, RGB, HSL and HSV, blends against another colour, and reports its own luminance. The named constants are the handful worth not spelling out every time.
Constants
TRANSPARENT
imagine.TRANSPARENT
BLACK
imagine.BLACK: Color
Opaque black.
WHITE
imagine.WHITE: Color
Opaque white.
GRAY
imagine.GRAY: Color
Opaque mid grey.
RED
imagine.RED: Color
Opaque pure red.
GREEN
imagine.GREEN: Color
Opaque pure green.
This is the additive primary, not CSS’s green, which is half as
bright. Color.named('green') gives that one.
BLUE
imagine.BLUE: Color
Opaque pure blue.
YELLOW
imagine.YELLOW: Color
Opaque yellow.
CYAN
imagine.CYAN: Color
Opaque cyan.
MAGENTA
imagine.MAGENTA: Color
Opaque magenta.
Classes
Color
class imagine.Color
An 8-bit RGBA colour.
Alpha runs 0 (fully transparent) to 255 (fully opaque), the same
convention as CSS, PNG and every modern image format. Colours are
immutable: every method that would change one returns a new Color, so
a colour held in a variable is safe to pass around and reuse.
import imagine { Color }
var red = Color(255, 0, 0)
var glass = red.with_alpha(128)
var brand = Color.hex('#4f46e5')
Relationship to the colors module
The colour space conversions here are the standard library’s, from
[[colors]]; Color is the value type that carries a result around and
puts it into a pixel buffer. Anything colors can do to a hexadecimal
string can be done to a Color by way of to_hex() and Color.hex().
Three conventions follow from that and are worth knowing:
-
Hue is in degrees and saturation, lightness and value are in percentage points (0-100), exactly as in
colorsand in CSS. They are not 0-1 ratios. -
to_hex()returns a leading#, because that is the form you paste into a stylesheet.colorsreturns the bare form. Both are accepted everywhere either is. -
A malformed hexadecimal colour or an unknown colour name raises
ValueError, which is whatcolorsreports for them, rather than this module’s ownImageError. An argument of the wrong type raisesTypeErrorfrom the declaration it failed. -
printable — has a
@to_string(), soechoandprint()show something useful -
serializable — has a
@to_json(), so it can be handed straight tojson.encode()
Fields
| Field | Type | Description |
|---|---|---|
r | number | The red channel, 0-255. |
g | number | The green channel, 0-255. |
b | number | The blue channel, 0-255. |
a | number | The alpha channel, 0 (transparent) to 255 (opaque). |
Constructor
imagine.Color(r, g, b, a)
Creates a colour from its channels.
Values outside 0-255 are clamped rather than rejected, and fractional values are rounded.
Parameters
r(number) — The red channel.g(number) — The green channel.b(number) — The blue channel.a(?number) — The alpha channel - Default: 255 (opaque).
Color.rgb()
imagine.Color.rgb(r, g, b) -> Color
Creates an opaque colour from red, green and blue channels.
The same as calling the constructor with three arguments; it exists so a
call site that also uses Color.hsl() or Color.lab() can name the
space it is working in.
Parameters
r(number)g(number)b(number)
Returns Color
Color.rgba()
imagine.Color.rgba(r, g, b, a) -> Color
Creates a colour from red, green, blue and alpha channels.
Parameters
r(number)g(number)b(number)a(number)
Returns Color
Color.gray()
imagine.Color.gray(value, a) -> Color
Creates a shade of grey.
Parameters
value(number) — The brightness, 0 (black) to 255 (white).a(?number) — The alpha channel - Default: 255.
Returns Color
Color.packed()
imagine.Color.packed(value: number) -> Color
Creates a colour from a packed 0xRRGGBBAA integer.
Alpha is in the least significant byte, which is the layout
to_packed() produces and the one this module passes around internally.
%> Color.packed(0xFF0000FF).to_hex()
'#ff0000'
Parameters
value(number)
Returns Color
Color.hex()
imagine.Color.hex(spec) -> Color
Creates a colour from a CSS-style hexadecimal string.
All four CSS lengths are accepted, with or without the leading #, in
either case: #rgb, #rgba, #rrggbb and #rrggbbaa. The short forms
double each digit, so #f0a is #ff00aa.
Parameters
spec(string)
Returns Color
Raises ValueError When spec is not a hexadecimal colour.
Color.named()
imagine.Color.named(name) -> Color
Looks up one of the CSS named colours.
Matching ignores case, spaces, hyphens and underscores, so 'Dark Sea Green'
and 'darkseagreen' are the same colour. The full CSS Color Level 4
list is available, including both spellings of the greys and
transparent.
Parameters
name(string)
Returns Color
Raises ValueError When name is not a CSS colour name.
Color.hsl()
imagine.Color.hsl(h, s, l, a) -> Color
Creates a colour from hue, saturation and lightness.
Parameters
h(number) — The hue in degrees. Wraps, so 400 is the same as 40.s(number) — The saturation in percentage points, 0-100.l(number) — The lightness in percentage points, 0 (black) to 100 (white).a(?number) — The alpha channel, 0-255 - Default: 255.
Returns Color
Color.hsv()
imagine.Color.hsv(h, s, v, a) -> Color
Creates a colour from hue, saturation and value (also called HSB).
Parameters
h(number) — The hue in degrees.s(number) — The saturation in percentage points, 0-100.v(number) — The value in percentage points, 0-100.a(?number) — The alpha channel, 0-255 - Default: 255.
Returns Color
Color.hwb()
imagine.Color.hwb(h, w, b, a) -> Color
Creates a colour from hue, whiteness and blackness.
Parameters
h(number) — The hue in degrees.w(number) — The whiteness in percentage points, 0-100.b(number) — The blackness in percentage points, 0-100.a(?number) — The alpha channel, 0-255 - Default: 255.
Returns Color
Color.cmyk()
imagine.Color.cmyk(c, m, y, k, a) -> Color
Creates a colour from cyan, magenta, yellow and key components.
This is the naive conversion, not a colour-managed one: it takes no account of an output profile, so it is right for generating colours and wrong for predicting what a printing press will do.
Parameters
c(number) — 0-100.m(number) — 0-100.y(number) — 0-100.k(number) — 0-100.a(?number) — The alpha channel, 0-255 - Default: 255.
Returns Color
Color.xyz()
imagine.Color.xyz(x, y, z, a) -> Color
Creates a colour from CIE XYZ tristimulus values, against the D65 white point.
Parameters
x(number)y(number)z(number)a(?number) — The alpha channel, 0-255 - Default: 255.
Returns Color
Color.lab()
imagine.Color.lab(l, a_star, b_star, a) -> Color
Creates a colour from CIE Lab*, against the D65 white point.
Lab is perceptually uniform, which makes it the right space for interpolating between two colours when the intermediate steps need to look evenly spaced.
Parameters
l(number) — Lightness, 0-100.a_star(number) — The green-red axis.b_star(number) — The blue-yellow axis.a(?number) — The alpha channel, 0-255 - Default: 255.
Returns Color
Color.parse()
imagine.Color.parse(value) -> Color
Turns whatever a caller passed into a Color.
Every drawing method calls this on its colour argument, so a colour can be written the shortest way that reads clearly at the call site. Accepted forms:
- a
Color, returned unchanged - a hexadecimal string,
'#4f46e5'or'4f46e5' - a CSS colour name,
'rebeccapurple' - a packed
0xRRGGBBAAnumber - a list of 3 or 4 channels,
[255, 0, 0]
Parameters
value(Color|string|number|list)
Returns Color
Raises ImageError When value is not a colour in any of those
forms.
Color.to_packed()
imagine.Color.to_packed() -> number
Returns the colour as a packed 0xRRGGBBAA integer.
Returns number
Color.to_hex()
imagine.Color.to_hex(always_alpha) -> string
Returns the colour as a hexadecimal string with a leading #.
The alpha digits are included only when the colour is not fully opaque,
unless always_alpha asks for them, so the common case reads as the
familiar six digits.
%> Color(255, 0, 0).to_hex()
'#ff0000'
%> Color(255, 0, 0, 128).to_hex()
'#ff000080'
Parameters
always_alpha(?bool) — Always emit the alpha digits - Default: false.
Returns string
Color.to_hsl()
imagine.Color.to_hsl() -> dict
Returns the colour in HSL, as a dictionary holding h (degrees), s
and l (percentage points) and a (0-255).
Returns dict
Color.to_hsv()
imagine.Color.to_hsv() -> dict
Returns the colour in HSV, as a dictionary holding h (degrees), s
and v (percentage points) and a (0-255).
Returns dict
Color.to_hwb()
imagine.Color.to_hwb() -> dict
Returns the colour in HWB, as a dictionary holding h (degrees), w
and b (percentage points) and a (0-255).
Returns dict
Color.to_cmyk()
imagine.Color.to_cmyk() -> dict
Returns the colour in CMYK, as a dictionary holding c, m, y and
k (percentage points) and a (0-255).
Returns dict
Color.to_xyz()
imagine.Color.to_xyz() -> dict
Returns the colour in CIE XYZ, as a dictionary holding x, y, z and
a (0-255).
Returns dict
Color.to_lab()
imagine.Color.to_lab() -> dict
Returns the colour in CIE Lab*, as a dictionary holding l, a_star,
b_star and a (0-255).
The axes are named a_star and b_star rather than a and b so
neither collides with the alpha channel.
Returns dict
Color.with_alpha()
imagine.Color.with_alpha(a) -> Color
Returns a copy of the colour with a different alpha channel.
Parameters
a(number) — The new alpha, 0-255.
Returns Color
Color.fade()
imagine.Color.fade(factor) -> Color
Returns a copy of the colour with its alpha scaled by a factor.
A factor of 0.5 halves whatever opacity the colour already had, rather than setting it to half opaque, so fading an already translucent colour behaves the way stacking two layers would.
Parameters
factor(number) — 0 (transparent) to 1 (unchanged).
Returns Color
Color.lighten()
imagine.Color.lighten(amount) -> Color
Returns a lighter version of the colour, keeping its hue.
Parameters
amount(number) — Percentage points of HSL lightness to add, 0-100.
Returns Color
Color.darken()
imagine.Color.darken(amount) -> Color
Returns a darker version of the colour, keeping its hue.
Parameters
amount(number) — Percentage points of HSL lightness to remove, 0-100.
Returns Color
Color.saturate()
imagine.Color.saturate(amount) -> Color
Returns a more saturated version of the colour.
Parameters
amount(number) — Percentage points of HSL saturation to add, 0-100.
Returns Color
Color.desaturate()
imagine.Color.desaturate(amount) -> Color
Returns a less saturated version of the colour.
An amount of 100 removes all colour, which is not the same as
to_grayscale(): this keeps HSL lightness, that one weights the
channels for perceived brightness.
Parameters
amount(number) — Percentage points of HSL saturation to remove, 0-100.
Returns Color
Color.rotate_hue()
imagine.Color.rotate_hue(degrees) -> Color
Returns the colour rotated around the hue wheel.
Parameters
degrees(number) — How far to rotate. Negative rotates backwards.
Returns Color
Color.mix()
imagine.Color.mix(other, t) -> Color
Returns a linear blend of this colour and another.
All four channels are interpolated, alpha included, so mixing a transparent colour in also makes the result more transparent.
Parameters
other(Color|string|number|list) — The colour to blend toward.t(number) — 0 returns this colour, 1 returns other.
Returns Color
Color.over()
imagine.Color.over(backdrop) -> Color
Composites this colour over an opaque backdrop and returns the flattened result.
This is what happens when a translucent colour is drawn onto a solid background, and it is how a colour has to be resolved before writing it to a format with no alpha channel, such as JPEG.
Parameters
backdrop(Color|string|number|list)
Returns Color
Color.luminance()
imagine.Color.luminance() -> number
Returns the relative luminance of the colour, 0 (black) to 1 (white), as defined by WCAG 2.
The alpha channel is ignored: luminance is a property of the colour
itself, and a translucent colour’s apparent brightness depends on
whatever is behind it. Composite with over() first if that matters.
Returns number
Color.contrast_ratio()
imagine.Color.contrast_ratio(other) -> number
Returns the WCAG 2 contrast ratio between this colour and another, from 1 (identical) to 21 (black against white).
Text is generally held to need a ratio of at least 4.5 against its background, or 3 at large sizes.
Parameters
other(Color|string|number|list)
Returns number
Color.is_dark()
imagine.Color.is_dark() -> bool
Returns true when the colour is dark enough that light text reads
better on it than dark text.
The threshold is the luminance at which contrast against white overtakes contrast against black.
Returns bool
Color.is_light()
imagine.Color.is_light() -> bool
The opposite of is_dark().
Returns bool
Color.best_contrast()
imagine.Color.best_contrast(light, dark) -> Color
Returns whichever of two colours reads better on top of this one.
Parameters
light(?Color|string) — The colour to use on a dark background - Default: white.dark(?Color|string) — The colour to use on a light background - Default: black.
Returns Color
Color.to_grayscale()
imagine.Color.to_grayscale() -> Color
Returns the grey with the same perceived brightness as this colour, keeping its alpha.
This uses the Rec. 601 luma weights, which is what image editors mean by
“greyscale” and what Image.grayscale() applies. It is deliberately not
[[colors.grayscale]], which sets HSL saturation to zero and so keeps a
bright yellow as bright as a dark blue.
Returns Color
Color.invert()
imagine.Color.invert() -> Color
Returns the colour with its red, green and blue channels inverted.
Alpha is left alone, since inverting it would turn a visible colour invisible rather than changing how it looks.
Returns Color
Color.equals()
imagine.Color.equals(other) -> bool
Returns true when both colours have identical channels.
Parameters
other(any)
Returns bool
Color.to_list()
imagine.Color.to_list() -> list
Returns the colour as a list of its four channels.
Returns list
Color.to_dict()
imagine.Color.to_dict() -> dict
Returns the colour as a dictionary holding r, g, b and a.
Returns dict
Color.to_string()
imagine.Color.to_string()
2026, Richard Ore and Zuri contributors
imagine.constants
import imagine
Everything here is re-exported by
imagine, soimport imagineis enough and the names are called asimagine.*. Importingimagine.constantson its own works too and reaches the same definitions.
Every named constant the module takes as an argument: resampling filters, blend modes, line caps, edge handling, file formats, anchor points, text alignment and flip axes.
They are gathered here so that a call reads as imagine.BILINEAR rather
than a bare number whose meaning has to be looked up.
Constants
NEAREST
imagine.NEAREST: string = 'nearest'
Nearest-neighbour. Picks the single closest source pixel and copies it, so it is the fastest filter and the only one that keeps hard edges perfectly hard.
Right for pixel art, QR codes, and any image where a blurred edge would be wrong. Wrong for photographs, where it produces visible stair-stepping.
BILINEAR
imagine.BILINEAR: string = 'bilinear'
Bilinear. Averages the four surrounding pixels.
A good default when speed matters more than sharpness. Enlarging with it looks soft; shrinking by more than about half loses detail, because it samples too few source pixels to represent what it discards.
BICUBIC
imagine.BICUBIC: string = 'bicubic'
Bicubic (Catmull-Rom). Fits a curve through sixteen surrounding pixels.
Noticeably sharper than bilinear when enlarging, at a few times the cost. A reasonable default for photographs.
GAUSSIAN
imagine.GAUSSIAN: string = 'gaussian'
Gaussian. Weights the neighbourhood by a bell curve.
Softer than bicubic on purpose. Useful when the result will be sharpened afterwards, or when resampling noisy input where a sharper filter would emphasise the noise.
LANCZOS
imagine.LANCZOS: string = 'lanczos'
Lanczos with a 3-lobe window. The highest quality filter here and the slowest.
The usual choice for thumbnails, where the image is being shrunk a long way and every remaining pixel matters. It can produce faint ringing next to very high-contrast edges, which is the price of its sharpness.
BLEND_NORMAL
imagine.BLEND_NORMAL: string = 'normal'
Ordinary alpha compositing: the source is painted over the destination according to its alpha. This is the default for every drawing and compositing operation.
BLEND_MULTIPLY
imagine.BLEND_MULTIPLY: string = 'multiply'
Multiplies the two colours. The result is never lighter than either input, so white leaves the backdrop alone and black forces black. The usual way to paint a shadow.
BLEND_SCREEN
imagine.BLEND_SCREEN: string = 'screen'
The inverse of multiply. The result is never darker than either input, so black leaves the backdrop alone and white forces white. The usual way to paint a glow.
BLEND_OVERLAY
imagine.BLEND_OVERLAY: string = 'overlay'
Multiply where the backdrop is dark, screen where it is light. Increases contrast while keeping highlights and shadows.
BLEND_DARKEN
imagine.BLEND_DARKEN: string = 'darken'
Keeps whichever channel is darker.
BLEND_LIGHTEN
imagine.BLEND_LIGHTEN: string = 'lighten'
Keeps whichever channel is lighter.
BLEND_COLOR_DODGE
imagine.BLEND_COLOR_DODGE: string = 'color_dodge'
Brightens the backdrop toward the source. Strong effect; saturates quickly.
BLEND_COLOR_BURN
imagine.BLEND_COLOR_BURN: string = 'color_burn'
Darkens the backdrop toward the source. The counterpart of dodge.
BLEND_HARD_LIGHT
imagine.BLEND_HARD_LIGHT: string = 'hard_light'
Overlay with the roles of source and backdrop swapped.
BLEND_SOFT_LIGHT
imagine.BLEND_SOFT_LIGHT: string = 'soft_light'
A gentler hard light, as if the source were a diffuse spotlight.
BLEND_DIFFERENCE
imagine.BLEND_DIFFERENCE: string = 'difference'
The absolute difference between the two colours. Identical images blended this way come out black, which makes it a quick visual diff.
BLEND_EXCLUSION
imagine.BLEND_EXCLUSION: string = 'exclusion'
Like difference, but lower contrast in the midtones.
BLEND_ADD
imagine.BLEND_ADD: string = 'add'
Adds the channels, clamped at white. Also called linear dodge.
BLEND_SUBTRACT
imagine.BLEND_SUBTRACT: string = 'subtract'
Subtracts the source from the backdrop, clamped at black.
CAP_BUTT
imagine.CAP_BUTT: string = 'butt'
The stroke stops dead at its endpoint. Nothing is drawn past the coordinates given.
The right choice when segments have to meet exactly, when a stroke’s length must be precisely what was asked for, or for the pieces of a dashed line.
CAP_ROUND
imagine.CAP_ROUND: string = 'round'
The stroke ends in a half-disc, so it reaches half its width past the endpoint. The default.
CAP_SQUARE
imagine.CAP_SQUARE: string = 'square'
The stroke ends in a square, so it reaches half its width past the endpoint, the same distance a round cap does but with corners.
EDGE_CLAMP
imagine.EDGE_CLAMP: string = 'clamp'
Pixels off the edge take the value of the nearest edge pixel. The default, and what keeps a blur from darkening the border.
EDGE_TRANSPARENT
imagine.EDGE_TRANSPARENT: string = 'transparent'
Pixels off the edge are treated as transparent black. Correct when the image really does end there and you want the blur to fade out.
EDGE_WRAP
imagine.EDGE_WRAP: string = 'wrap'
Pixels off the edge wrap to the opposite side. For images meant to tile seamlessly.
PNG
imagine.PNG: string = 'png'
PNG. Lossless, alpha, universally supported. The right default for anything with sharp edges, text or transparency.
JPEG
imagine.JPEG: string = 'jpeg'
JPEG. Lossy, no alpha. The right choice for photographs, and the wrong
one for anything with hard edges. Transparent pixels are flattened
against the background option on export, white unless you say
otherwise.
GIF
imagine.GIF: string = 'gif'
GIF. At most 256 colours per frame, one-bit transparency, and the only animated format this module can write.
BMP
imagine.BMP: string = 'bmp'
BMP. Uncompressed and enormous, but readable by anything.
TIFF
imagine.TIFF: string = 'tiff'
TIFF. Lossless, alpha, common in printing and scanning workflows.
TGA
imagine.TGA: string = 'tga'
Truevision TGA. Lossless with alpha, still common in game asset pipelines.
WEBP
imagine.WEBP: string = 'webp'
WebP. Lossless with alpha, and smaller than PNG for the same pixels.
Reading handles lossy and lossless WebP, still and animated. Writing
produces a lossless still, so the quality option does not apply and a
photograph written here will be larger than one a lossy WebP encoder
would produce. Reach for JPEG or AVIF when size matters more than
exactness.
AVIF
imagine.AVIF: string = 'avif'
AVIF. The smallest files of anything here at a given quality.
Write only. Opening an AVIF raises DecodeError, and
[[imagine.capabilities]] lists AVIF under encode but not decode, so
a program can check rather than discover it at run time.
QOI
imagine.QOI: string = 'qoi'
QOI, the Quite OK Image format. Lossless with alpha, encodes and decodes several times faster than PNG at a modest size penalty. Good for caches and intermediate files.
ICO
imagine.ICO: string = 'ico'
Windows ICO. An icon container; a single image is written as one entry.
PNM
imagine.PNM: string = 'pnm'
Netpbm (PBM, PGM, PPM). Trivially simple and trivially large.
WBMP
imagine.WBMP: string = 'wbmp'
Wireless Bitmap. One bit per pixel, no compression, no alpha.
Writing thresholds each pixel on its perceived brightness, so a colour image comes out as a black and white silhouette. Reading gives opaque black and white pixels.
TOP_LEFT
imagine.TOP_LEFT: string = 'top_left'
Where a smaller image or a piece of text sits inside a larger box, used
by Image.cover(), Image.contain() and Image.place().
TOP
imagine.TOP: string = 'top'
TOP_RIGHT
imagine.TOP_RIGHT: string = 'top_right'
LEFT
imagine.LEFT: string = 'left'
CENTER
imagine.CENTER: string = 'center'
RIGHT
imagine.RIGHT: string = 'right'
BOTTOM_LEFT
imagine.BOTTOM_LEFT: string = 'bottom_left'
BOTTOM
imagine.BOTTOM: string = 'bottom'
BOTTOM_RIGHT
imagine.BOTTOM_RIGHT: string = 'bottom_right'
ALIGN_LEFT
imagine.ALIGN_LEFT: string = 'left'
Lines of a multi-line string start at the same left edge. The default.
ALIGN_CENTER
imagine.ALIGN_CENTER: string = 'center'
Lines of a multi-line string are centred against each other.
ALIGN_RIGHT
imagine.ALIGN_RIGHT: string = 'right'
Lines of a multi-line string end at the same right edge.
FLIP_HORIZONTAL
imagine.FLIP_HORIZONTAL: number = 1
Mirror left to right.
FLIP_VERTICAL
imagine.FLIP_VERTICAL: number = 2
Mirror top to bottom.
FLIP_BOTH
imagine.FLIP_BOTH: number = 3
Mirror both ways at once, which is the same as rotating 180 degrees.
2026, Richard Ore and Zuri contributors
imagine.errors
import imagine
Everything here is re-exported by
imagine, soimport imagineis enough and the names are called asimagine.*. Importingimagine.errorson its own works too and reaches the same definitions.
Every error the module raises, under ImageError as their root.
The subclasses separate the cases a caller treats differently: input that is not an image it can read, output it cannot write, a format it does not know, a coordinate outside the surface, and a font that will not load.
Classes
ImageError
class imagine.ImageError < Error
Base class for every error the imagine module raises.
Catching ImageError catches everything this module throws on its own.
The subclasses below exist so a caller can tell an unreadable upload
apart from a coordinate mistake in their own code without matching on
message text.
- printable — has a
@to_string(), soechoandprint()show something useful
ImageError.to_string()
imagine.ImageError.to_string()
DecodeError
class imagine.DecodeError < ImageError
Raised when image data cannot be read: the bytes are not an image at all, the format is one this build cannot decode, or the file is truncated or corrupt.
Anything arriving from outside the program (an upload, a download, a file a user picked) can raise this, so it is the one error a server handling images must always be ready for.
- printable — has a
@to_string(), soechoandprint()show something useful
DecodeError.to_string()
imagine.DecodeError.to_string()
EncodeError
class imagine.EncodeError < ImageError
Raised when an image cannot be written in the requested format, either because this build has no encoder for it or because the encoder rejected the image or the options given.
- printable — has a
@to_string(), soechoandprint()show something useful
EncodeError.to_string()
imagine.EncodeError.to_string()
FormatError
class imagine.FormatError < ImageError
Raised when a format name is not one imagine knows, or when a file
extension cannot be mapped to a format.
- printable — has a
@to_string(), soechoandprint()show something useful
FormatError.to_string()
imagine.FormatError.to_string()
BoundsError
class imagine.BoundsError < ImageError
Raised when a rectangle, crop or resize falls outside the image, or when a dimension is zero or negative.
Drawing operations do not raise this. A line running off the edge of the canvas is clipped, which is what every drawing API does and what callers expect; it is only the operations that must return an image of an exact size that have no sensible way to continue.
- printable — has a
@to_string(), soechoandprint()show something useful
BoundsError.to_string()
imagine.BoundsError.to_string()
FontError
class imagine.FontError < ImageError
Raised when a font cannot be parsed, cannot be found on the system, or does not carry the horizontal metrics text layout needs.
- printable — has a
@to_string(), soechoandprint()show something useful
FontError.to_string()
imagine.FontError.to_string()
2026, Richard Ore and Zuri contributors
imagine.filters
import imagine
imagineexposes this asimagine.filters, soimport imagineis enough and the names are called asimagine.filters.*.import imagine.filtersreaches the same definitions directly.
The maths behind the filters: colour matrices, lookup tables and convolution kernels.
Image exposes these as named methods, and this is where the numbers
those methods pass come from. Building a matrix, a LUT or a kernel here
and applying it yourself is how to get a filter the module does not
already name.
Constants
LUMA
imagine.filters.LUMA: list = [...]
Rec. 601 luma weights.
These are what “grayscale” means to an image editor, and what
[[imagine.Color.to_grayscale]] uses, so a grey produced by
Image.grayscale() and one produced by Color.to_grayscale() agree.
They are deliberately not the WCAG linear-light weights that
[[colors.relative_luminance]] uses; those answer a different question,
about contrast rather than appearance.
MATRIX_LUMA
imagine.filters.MATRIX_LUMA: list = [...]
The luma weights the colour-matrix filters use, from the SVG and CSS filter specifications.
Saturation and hue rotation are defined against these, so a saturate()
here matches what a browser does to the same image.
Functions
identity_matrix()
imagine.filters.identity_matrix() -> list
The identity colour matrix: applying it changes nothing.
A useful starting point for building one by hand.
Returns list
grayscale_matrix()
imagine.filters.grayscale_matrix() -> list
A colour matrix that collapses every colour to its grey of equal perceived brightness.
Returns list
sepia_matrix()
imagine.filters.sepia_matrix() -> list
A colour matrix approximating the warm brown cast of a sepia photograph.
The coefficients are the widely used Microsoft ones, which are what most software means by “sepia”.
Returns list
saturation_matrix()
imagine.filters.saturation_matrix(amount: number) -> list
A colour matrix that scales saturation.
An amount of 0 removes all colour, 1 leaves the image alone, and values above 1 push saturation past its original level. Amounts below 0 pass through the grey point and out the other side, which inverts hues; that is well defined but rarely what anyone wants.
Parameters
amount(number)
Returns list
hue_rotate_matrix()
imagine.filters.hue_rotate_matrix(degrees: number) -> list
A colour matrix that rotates every hue around the colour wheel by an angle in degrees, leaving brightness and saturation alone.
This is the rotation from the SVG filter specification, which approximates a true rotation in a luma-preserving space closely enough that the difference is not visible.
Parameters
degrees(number) — Positive rotates toward red, negative away from it.
Returns list
duotone_matrix()
imagine.filters.duotone_matrix(shadow, highlight) -> list
A colour matrix that replaces every pixel’s colour with a blend between two colours chosen by its brightness, a duotone.
Dark pixels take shadow, light ones take highlight, and everything between is interpolated. Alpha is untouched.
Parameters
shadow(Color|string|number) — The colour black maps to.highlight(Color|string|number) — The colour white maps to.
Returns list
combine_matrices()
imagine.filters.combine_matrices(first: list, second: list) -> list
Multiplies two colour matrices, giving one matrix with the effect of applying first and then second.
Combining matrices this way and applying the result once is both faster and more accurate than applying each in turn, because the intermediate result is never rounded back to 8 bits.
Parameters
first(list)second(list)
Returns list
build_lut()
imagine.filters.build_lut(fn: function) -> bytes
Builds a 256-entry lookup table from a function.
The function is called once per possible input value, 0 through 255, and whatever it returns is clamped to a byte. This is the general way to write a tone curve: build the table once, and the image is then rewritten at memory speed no matter how large it is.
import imagine { filters }
# A steep S-curve, for contrast that keeps its highlights.
var curve = filters.build_lut(@(value) {
var t = value / 255
return 255 * t * t * (3 - 2 * t)
})
Parameters
fn(function(1)) — Called with an input value, returns the output.
Returns bytes
identity_lut()
imagine.filters.identity_lut() -> bytes
A lookup table that leaves every value alone.
Returns bytes
brightness_lut()
imagine.filters.brightness_lut(delta: number) -> bytes
A lookup table that adds a fixed amount to every value.
Parameters
delta(number) — -255 to 255. Positive brightens.
Returns bytes
contrast_lut()
imagine.filters.contrast_lut(amount: number) -> bytes
A lookup table that pushes values away from or toward mid-grey.
The amount runs -100 (everything collapses to grey) through 0 (no change) to 100 (everything is forced to black or white). This is the contrast curve most image editors use.
Parameters
amount(number) — -100 to 100.
Returns bytes
gamma_lut()
imagine.filters.gamma_lut(gamma: number) -> bytes
A lookup table applying a gamma curve.
Values below 1 darken the midtones and values above 1 lighten them, while black and white stay put. Correcting an image encoded at one gamma for display at another is the usual reason to reach for this.
Parameters
gamma(number) — Greater than zero.
Returns bytes
invert_lut()
imagine.filters.invert_lut() -> bytes
A lookup table that inverts every value.
Returns bytes
threshold_lut()
imagine.filters.threshold_lut(level: number) -> bytes
A lookup table that forces every value to black or white.
Parameters
level(number) — Values below this become 0, the rest become 255.
Returns bytes
posterize_lut()
imagine.filters.posterize_lut(levels: number) -> bytes
A lookup table that reduces each channel to a fixed number of evenly spaced steps.
Parameters
levels(number) — 2 or more. Two gives a hard black and white split.
Returns bytes
scale_lut()
imagine.filters.scale_lut(factor: number) -> bytes
A lookup table that scales every value by a factor.
Applied to the alpha channel, this is what Image.opacity() does.
Parameters
factor(number) — 0 to 1, though larger values are allowed and clamp.
Returns bytes
levels_lut()
imagine.filters.levels_lut(black: number, white: number, gamma: ?number) -> bytes
A lookup table implementing a levels adjustment.
Everything at or below black becomes 0, everything at or above white becomes 255, and the range between is stretched to fill the whole scale with gamma applied along the way. This is the single most useful correction for a flat or washed-out photograph.
Parameters
black(number) — The input value that becomes black, 0-255.white(number) — The input value that becomes white, 0-255.gamma(?number) — The midtone curve - Default: 1 (linear).
Returns bytes
sharpen_kernel()
imagine.filters.sharpen_kernel(strength) -> list
A 3x3 sharpening kernel.
The strength is how much the centre pixel is emphasised over its neighbours; 1 is a moderate sharpen and values much above 3 start producing halos around edges.
Parameters
strength(?number) — Default: 1.
Returns list
box_blur_kernel()
imagine.filters.box_blur_kernel() -> list
A 3x3 box blur kernel; every neighbour counts equally.
Cheaper than a Gaussian and visibly boxier. Image.blur() uses a real
Gaussian instead; this is here for the cases that want the harder look,
or a single very cheap pass.
Returns list
emboss_kernel()
imagine.filters.emboss_kernel() -> list
A 3x3 kernel that lifts edges into a grey relief.
Used with an offset of 128, so flat areas come out mid-grey rather than
black; Image.emboss() supplies that offset.
Returns list
edge_kernel()
imagine.filters.edge_kernel() -> list
A 3x3 Laplacian kernel that leaves only the edges.
Its weights sum to zero, so it needs an explicit divisor of 1 rather than the usual “divide by the sum”.
Returns list
mean_removal_kernel()
imagine.filters.mean_removal_kernel() -> list
A 3x3 kernel that removes local mean, exaggerating detail.
Returns list
smooth_kernel()
imagine.filters.smooth_kernel(weight) -> list
A 3x3 smoothing kernel weighted toward the centre pixel.
A larger weight keeps more of the original and smooths less.
Parameters
weight(?number) — The centre weight - Default: 8.
Returns list
gaussian_kernel()
imagine.filters.gaussian_kernel(sigma: number) -> list
A square Gaussian kernel with the given standard deviation.
Image.blur() uses a separable Gaussian instead, which is far faster
for anything but the smallest radius; this exists for code that wants
the kernel itself, to combine with another.
Parameters
sigma(number) — Greater than zero.
Returns list
2026, Richard Ore and Zuri contributors
imagine.font
import imagine.font
imaginelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledimagine.font.*needsimport imagine.font.
Font: a loaded TrueType or OpenType face, ready to measure and draw
text with.
Glyph rasterisation and metrics come from the font file itself, so kerning and line height are whatever the designer specified rather than an approximation.
Classes
Font
class imagine.Font
A typeface at a particular size, ready to draw with.
A Font is immutable and cheap to copy: size() hands back another
Font sharing the same parsed face, so keeping one font around and
asking it for several sizes costs nothing extra.
import imagine { Image, Font, Color }
var title = Font.load('assets/Inter.ttf').size(32)
var body = title.size(14)
Image(400, 120, Color.WHITE)
.text(20, 20, 'Quarterly report', title, '#111111')
.text(20, 64, 'Revenue is up 12% year on year.', body, '#555555')
.save('report.png')
What text layout does and does not do
Glyphs are positioned by advance width with kerning applied, and \n
starts a new line. That covers Latin, Greek, Cyrillic and anything else
written left to right without contextual shaping.
It does not do complex shaping: Arabic letters will not join, Indic clusters will not reorder, and ligatures are not substituted. Those need a shaping engine, and a standard library that pretended to do them would be worse than one that says plainly it does not.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
imagine.Font(handle, size, source)
Wraps an already-parsed face.
Not the way to get a Font; use load(), from_bytes(), system() or
default(), all of which end up here.
Parameters
ptr— handlesize(number)source(string)
Font.load()
imagine.Font.load(path: string, size) -> Font
Loads a TrueType or OpenType font from a file.
The parsed face is cached by absolute path, so loading the same file twice in one process parses it once.
Parameters
path(string) — A path to a.ttf,.otfor.ttcfile.size(?number) — The size in pixels - Default: 16.
Returns Font
Font.from_bytes()
imagine.Font.from_bytes(data: bytes, size) -> Font
Parses a font already held in memory.
Useful for a font embedded in the program, downloaded, or read out of an archive. Nothing is cached, since there is no path to key a cache on; call this once and keep the result.
Parameters
data(bytes)size(?number) — The size in pixels - Default: 16.
Returns Font
Font.system()
imagine.Font.system(family, size) -> Font
Finds a font installed on this machine by family name.
The standard font directories for the platform are searched, and the
match ignores case, spaces and hyphens, so 'DejaVu Sans',
'dejavusans' and 'DejaVuSans' all find the same file. A family with
several weights matches whichever file the search reaches first, which
is why load() is the right call when a specific weight matters.
Raises FontError when nothing matches.
Parameters
family(string)size(?number) — The size in pixels - Default: 16.
Returns Font
Font.sans()
imagine.Font.sans(size) -> Font
Finds a reasonable sans-serif font installed on this machine.
A short list of the families most likely to be installed is tried in turn. This is a convenience for scripts and tests, not something to rely on for output that has to look the same everywhere: which font it lands on depends entirely on the machine, and a container image with no fonts installed has none to find.
Raises FontError when the machine has no usable font, with a message
saying so rather than a parse failure.
Parameters
size(?number) — The size in pixels - Default: 16.
Returns Font
Font.builtin()
imagine.Font.builtin(size) -> StrokeFont
The built-in font, which is always available.
imagine ships no font file, so system() and sans() both depend on
what happens to be installed and can fail. This one cannot: it is drawn
from stroke geometry rather than loaded, and scales to any size.
The result is a [[imagine.StrokeFont]], not a Font. The two have the
same interface, so anywhere a font is accepted either works. Read that
class’s own documentation for what the built-in font is and is not
suitable for.
var font = Font.builtin(20)
chart.text(12, 8, 'requests / second', font, '#334155')
Parameters
size(?number) — The size in pixels - Default: 16.
Returns StrokeFont
Font.size()
imagine.Font.size(pixels: number) -> Font
Returns the same typeface at a different size.
The parsed face is shared, so this is cheap enough to call in a loop.
Parameters
pixels(number) — The size in pixels.
Returns Font
Font.pixel_size()
imagine.Font.pixel_size() -> number
The size in pixels this font draws at.
Returns number
Font.name()
imagine.Font.name() -> string
The typeface’s own name, as recorded in the font file.
Returns string
Font.source()
imagine.Font.source() -> string
Where this font came from: an absolute path, or '<memory>' for one
parsed from bytes.
Returns string
Font.metrics()
imagine.Font.metrics() -> dict
The font’s vertical metrics at its current size, in pixels.
The dictionary holds:
name: the typeface’s name.ascent: how far the tallest glyphs rise above the baseline.descent: how far descenders fall below it, as a negative number.line_gap: the designer’s recommended extra space between lines.line_height: ascent minus descent plus line gap, which is the distance between consecutive baselines at single spacing.
Returns dict
Font.measure()
imagine.Font.measure(text, options) -> dict
Measures a string without drawing it.
Returns a dictionary holding width and height in pixels, baseline
(the first line’s baseline, measured down from the top of the box) and
lines (how many lines the string has).
The box covers the text’s advance widths. A glyph can paint a fraction
of a pixel outside it, which is why Image.text() allows for a little
slack when it draws.
Parameters
text(string)options(?dict) —tracking,line_height,align.
Returns dict
Font.wrap()
imagine.Font.wrap(text, width: number, options) -> string
Breaks a string into lines that fit within a width.
Returns the same text with newlines inserted, ready to hand to
Image.text(). Newlines already in the text are kept as paragraph
breaks, and runs of spaces are collapsed to one.
Words are kept whole where they can be. A single word too long for the width is broken between characters rather than allowed to overflow, since overflowing text in an image cannot be scrolled to.
var body = Font.load('Inter.ttf', 16)
var text = body.wrap(article, 520)
page.text(40, 120, text, body, '#334155')
Parameters
text(string)width(number) — The maximum line width in pixels.options(?dict) —tracking, asmeasure()takes.
Returns string
Font.render()
imagine.Font.render(text, options) -> dict
Rasterizes a string into an 8-bit coverage mask.
Returns a dictionary holding coverage (the mask, one byte per pixel),
width, height and baseline. Drawing the mask through
Canvas.draw_mask() is what Image.text() does; this is here for code
that wants the mask itself, to use as a stencil or an alpha channel.
Parameters
text(string)options(?dict) —tracking,line_height,align.
Returns dict
Font.to_string()
imagine.Font.to_string()
2026, Richard Ore and Zuri contributors
imagine.formats
import imagine
imagineexposes this asimagine.formats, soimport imagineis enough and the names are called asimagine.formats.*.import imagine.formatsreaches the same definitions directly.
What the build can actually read and write, and how to tell one format from another.
detect() and probe() identify an image from its leading bytes rather
than its file name, which is the only reliable way to do it.
readable(), writable() and animatable() report what this
particular build supports, since that depends on which codecs were
compiled in.
Functions
from_extension()
imagine.formats.from_extension(path: string) -> ?string
The format name a file extension implies, or nil when the extension is
not one this module knows.
The argument may be a bare extension or a whole path, with or without a leading dot, in any case.
%> imagine.formats.from_extension('photo.JPG')
'jpeg'
%> imagine.formats.from_extension('.webp')
'webp'
Parameters
path(string)
Returns ?string
extension_for()
imagine.formats.extension_for(format: string) -> string
The file extension a format is normally written with, without a leading dot.
Parameters
format(string)
Returns string
Raises FormatError When format is not a known format.
mime_for()
imagine.formats.mime_for(format: string) -> string
The IANA media type for a format, suitable for a Content-Type header.
Parameters
format(string)
Returns string
Raises FormatError When format is not a known format.
normalize()
imagine.formats.normalize(format: string) -> string
Normalises a format name, accepting the common aliases.
'JPG', 'jpeg' and '.jpg' all come back as 'jpeg'.
Parameters
format(string)
Returns string
Raises FormatError When format is not a known format.
detect()
imagine.formats.detect(data: bytes) -> ?string
Identifies image data by its content rather than its name, or returns
nil when the bytes are not a recognisable image.
This reads only as much of the header as it needs, so it is cheap enough to run over an upload before deciding whether to decode it.
var kind = imagine.formats.detect(upload)
if kind == nil {
raise Exception('that is not an image')
}
Parameters
data(bytes)
Returns ?string
probe()
imagine.formats.probe(data: bytes) -> ?dict
Reads an image’s format and dimensions without decoding its pixels.
Returns a dictionary holding format, width and height, or nil
when the data is not a recognisable image. Reading a multi-megapixel
JPEG’s header costs a few microseconds where decoding it costs tens of
milliseconds, so this is the right way to reject an oversized upload
before committing to it.
Parameters
data(bytes)
Returns ?dict
readable()
imagine.formats.readable() -> list
Every format this build can read.
Returns list
writable()
imagine.formats.writable() -> list
Every format this build can write.
Returns list
animatable()
imagine.formats.animatable() -> list
Every format this build can read as an animation.
Returns list
decode_wbmp()
imagine.formats.decode_wbmp(data: bytes)
Decodes a WBMP into straight RGBA pixels.
Set bits become opaque white and clear bits opaque black; the format has no colour and no transparency.
Parameters
data(bytes)
Returns — dict: pixels, width, height.
Raises DecodeError When the data is not a well-formed WBMP.
encode_wbmp()
imagine.formats.encode_wbmp(pixels: bytes, width: number, height: number, threshold: ?number) -> bytes
Encodes straight RGBA pixels as a WBMP.
Each pixel is reduced to one bit by comparing its perceived brightness against threshold. Transparent pixels are treated as white, since the format has no way to say “nothing here” and white is what a viewer will show as background.
Parameters
pixels(bytes)width(number)height(number)threshold(?number) — The brightness cut, 0-255 - Default: 128.
Returns bytes
2026, Richard Ore and Zuri contributors
imagine.image
import imagine.image
imaginelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledimagine.image.*needsimport imagine.image.
Image: a raster image as an RGBA pixel buffer, and everything that
transforms one.
Resizing, cropping, rotation, compositing, colour adjustment and the named filters are all methods here. Every one produces a new image rather than altering the receiver, except where a method says otherwise.
Classes
Image
class imagine.Image < Canvas
A raster image: a rectangle of 8-bit RGBA pixels, everything that draws onto one, and everything that reshapes, filters or writes one out.
import imagine { Image, Color, LANCZOS }
Image.open('photo.jpg')
.thumbnail(400, 400, LANCZOS)
.save('thumb.webp')
Which operations copy and which do not
There is one rule, and it is worth learning because it is the only thing here that is not obvious:
- Anything that changes the image’s size returns a new
Image.resize(),crop(),rotate(),flip(),pad()and the rest leave the original untouched. - Everything else changes the image in place and returns it. Drawing, filters and compositing all work on the image you called them on, and return it so calls can be chained.
That means a chain mixing the two kinds still reads correctly, and
clone() is there for the times you want to keep the original before a
filter.
var original = Image.open('photo.jpg')
var small = original.thumbnail(200, 200) # original untouched
small.grayscale() # small is now grey
Memory
An image costs four bytes per pixel, so a 6000x4000 photograph is 96 MB in memory however small its file was. Reach for [[imagine.formats.probe]] before opening anything whose size you do not control.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
imagine.Image(width: number, height: number, fill)
Creates a new image.
The image starts fully transparent unless a fill colour is given.
Parameters
width(number) — At least 1.height(number) — At least 1.fill(?Color|string|number) — A colour to fill with - Default: transparent.
Raises BoundsError When either dimension is below 1.
Image.open()
imagine.Image.open(source, options) -> Image
Opens an image file.
The format is detected from the file’s contents, not its name, so a mislabelled file still opens correctly.
JPEGs carrying an EXIF orientation tag are rotated upright, since that
is what the photograph was meant to look like and almost never what the
raw pixels are. Pass { orient: false } to get the pixels exactly as
stored.
Parameters
source(string|file) — A path, or an already-open file.options(?dict) —orient(bool, default true),format(string).
Returns Image
Raises DecodeError When the file is not a readable image.
Image.decode()
imagine.Image.decode(data: bytes, options) -> Image
Decodes image data held in memory.
The format is detected from the data unless format says otherwise, in
which case a file that is not really that format fails loudly rather
than being decoded as whatever it actually is.
Parameters
data(bytes)options(?dict) —orient(bool, default true),format(string).
Returns Image
Raises DecodeError When the data is not a readable image.
Image.from_pixels()
imagine.Image.from_pixels(width, height, pixels, format) -> Image
Wraps an existing RGBA pixel buffer as an image.
The buffer is adopted, not copied, so whatever produced it must not keep writing to it. Pixels run red, green, blue, alpha with straight (not premultiplied) alpha and no row padding.
Parameters
width(number)height(number)pixels(bytes) — Exactlywidth * height * 4bytes.format(?string) — The format these pixels were decoded from, recorded as metadata - Default: nil.
Returns Image
Image.size()
imagine.Image.size() -> dict
The image’s dimensions as {width, height}.
Returns dict
Image.format()
imagine.Image.format() -> ?string
The format this image was decoded from, or nil for one created in
memory.
This records where the image came from and never constrains where it can go; a PNG can always be saved as a JPEG.
Returns ?string
Image.set_format()
imagine.Image.set_format(format)
Records which format this image should be considered to have come from.
The only thing this changes is what encode() and save() write when
no format is given. It is set for you when an image is decoded; setting
it by hand is for pixels that arrived some other way and whose origin
you happen to know.
Parameters
format(?string) — A format name, or nil to clear it.
Returns — self
Image.bounds()
imagine.Image.bounds() -> dict
The rectangle covering the whole image, as {x, y, width, height}.
Returns dict
Image.is_opaque()
imagine.Image.is_opaque() -> bool
true when every pixel is fully opaque.
This walks the alpha channel, so it costs a pass over the image. Worth knowing before choosing an output format: an opaque image loses nothing as a JPEG.
Returns bool
Image.info()
imagine.Image.info() -> dict
A summary of the image as a dictionary holding width, height,
format, pixels (the count, not the buffer) and opaque.
Returns dict
Image.clone()
imagine.Image.clone() -> Image
An independent copy of the image, sharing nothing with it.
Returns Image
Image.layer()
imagine.Image.layer(fill) -> Image
A new image of the same size, fully transparent.
The usual way to build up a composite: draw onto the layer, then bring
it back with draw_image() under whichever blend mode and opacity the
effect calls for.
Parameters
fill(?Color|string|number) — A colour to fill with - Default: transparent.
Returns Image
Image.resize()
imagine.Image.resize(width, height, filter) -> Image
Returns the image resampled to an exact size.
The aspect ratio is not preserved; the result is exactly the size asked
for. Use thumbnail() or cover() when the proportions matter.
Parameters
width(number) — At least 1.height(number) — At least 1.filter(?string) — A resampling filter - Default:LANCZOS.
Returns Image
Raises BoundsError When either dimension is below 1.
Image.scale()
imagine.Image.scale(factor: number, filter) -> Image
Returns the image resampled by a factor, keeping its proportions.
Parameters
factor(number) — Greater than zero. 0.5 halves each side.filter(?string) — A resampling filter - Default:LANCZOS.
Returns Image
Image.thumbnail()
imagine.Image.thumbnail(max_width, max_height, filter) -> Image
Returns the image shrunk to fit inside a box, keeping its proportions.
The result is no larger than the box in either direction, and usually smaller in one. An image that already fits is returned at its original size rather than enlarged, which is what makes this the right call for thumbnails: enlarging a small image to fill a thumbnail box only makes it blurry.
Parameters
max_width(number)max_height(number)filter(?string) — A resampling filter - Default:LANCZOS.
Returns Image
Image.fit()
imagine.Image.fit(width, height, filter) -> Image
Returns the image scaled to fit inside a box, keeping its proportions, enlarging it if it is smaller than the box.
The difference from thumbnail() is exactly that: this one will scale
up.
Parameters
width(number)height(number)filter(?string) — A resampling filter - Default:LANCZOS.
Returns Image
Image.cover()
imagine.Image.cover(width, height, options) -> Image
Returns the image filling a box exactly, keeping its proportions by cropping whatever overflows.
This is what a CSS background-size: cover does, and what almost every
avatar or card thumbnail wants: the box is filled edge to edge with no
distortion and no letterboxing, at the cost of losing some of the image.
Parameters
width(number)height(number)options(?dict) —anchor(where the kept part comes from, defaultCENTER),filter.
Returns Image
Image.contain()
imagine.Image.contain(width, height, options) -> Image
Returns the image fitted inside a box exactly, keeping its proportions by padding whatever is left over.
The counterpart to cover(): nothing is lost, but the result has bars
on two sides.
Parameters
width(number)height(number)options(?dict) —background(the padding colour, default transparent),anchor(defaultCENTER),filter.
Returns Image
Image.crop()
imagine.Image.crop(x, y, width, height) -> Image
Returns a rectangular region of the image.
The rectangle must lie entirely inside the image. Clamping a crop that
runs off the edge would hand back different dimensions than were asked
for, which is a worse surprise than an error; use pad() first if the
region really is meant to extend past the edge.
Parameters
x(number)y(number)width(number)height(number)
Returns Image
Raises BoundsError When the rectangle does not fit inside the
image.
Image.trim()
imagine.Image.trim(options) -> Image
Returns the image with a uniform border trimmed away.
The border colour is taken from the top-left pixel unless background
names one. tolerance is how far a pixel may differ from it and still
count as border, measured as the largest difference across the four
channels.
An image that is entirely border is returned as a 1x1 image rather than an empty one, since an image with no pixels is not a thing this module can represent.
Parameters
options(?dict) —background,tolerance(default 0).
Returns Image
Image.pad()
imagine.Image.pad(top, right, bottom, left, background) -> Image
Returns the image with a border added on each side.
Negative amounts are not accepted; use crop() to remove edges.
Parameters
top(number)right(?number) — Default: the same as top.bottom(?number) — Default: the same as top.left(?number) — Default: the same as right.background(?Color|string|number) — Default: transparent.
Returns Image
Image.rotate()
imagine.Image.rotate(degrees: number, background) -> Image
Returns the image rotated by an angle in degrees, clockwise.
Quarter turns are exact and lossless. Any other angle resamples the
pixels and grows the canvas to hold the rotated corners, filling the
gaps with background.
Parameters
degrees(number)background(?Color|string|number) — The colour behind the corners- Default: transparent.
Returns Image
Image.rotate_90()
imagine.Image.rotate_90() -> Image
Returns the image turned a quarter turn clockwise. Lossless.
Returns Image
Image.rotate_180()
imagine.Image.rotate_180() -> Image
Returns the image turned upside down. Lossless.
Returns Image
Image.rotate_270()
imagine.Image.rotate_270() -> Image
Returns the image turned a quarter turn anticlockwise. Lossless.
Returns Image
Image.flip()
imagine.Image.flip(mode) -> Image
Returns the image mirrored.
Parameters
mode(?number) —FLIP_HORIZONTAL,FLIP_VERTICALorFLIP_BOTH- Default:
FLIP_HORIZONTAL.
- Default:
Returns Image
Image.flip_horizontal()
imagine.Image.flip_horizontal() -> Image
Returns the image mirrored left to right.
Returns Image
Image.flip_vertical()
imagine.Image.flip_vertical() -> Image
Returns the image mirrored top to bottom.
Returns Image
Image.transpose()
imagine.Image.transpose() -> Image
Returns the image reflected across its main diagonal, so a w x h image
becomes h x w.
Returns Image
Image.apply_lut()
imagine.Image.apply_lut(red, green, blue, alpha)
Rewrites every pixel through up to four 256-entry lookup tables, one per channel.
This is the primitive behind most of the adjustments below, and it is the fastest way to apply any per-channel tone curve: the table is built once whatever the image’s size, and applying it is a single indexed load per channel.
A nil table leaves that channel alone. Tables are built with the
helpers in [[imagine.filters]], or with [[imagine.filters.build_lut]]
for a curve of your own.
Parameters
red(?bytes) — A 256-entry table, or nil.green(?bytes)blue(?bytes)alpha(?bytes)
Returns — self
Image.apply_matrix()
imagine.Image.apply_matrix(matrix)
Rewrites every pixel through a 4x5 colour matrix.
The matrix is a flat list of 20 numbers, laid out row by row, and is the same shape SVG and CSS filters use. The last entry of each row is a constant added afterwards, in 0-255 units.
r' = m0*r + m1*g + m2*b + m3*a + m4
g' = m5*r + m6*g + m7*b + m8*a + m9
b' = m10*r + m11*g + m12*b + m13*a + m14
a' = m15*r + m16*g + m17*b + m18*a + m19
Applying several matrices in a row is both slower and less accurate than combining them with [[imagine.filters.combine_matrices]] and applying the result once.
Parameters
matrix(list) — 20 numbers.
Returns — self
Image.brightness()
imagine.Image.brightness(delta)
Adds a fixed amount to every colour channel.
Parameters
delta(number) — -255 to 255. Positive brightens.
Returns — self
Image.contrast()
imagine.Image.contrast(amount)
Pushes colours away from or toward mid-grey.
Parameters
amount(number) — -100 (flat grey) through 0 (unchanged) to 100 (hard black and white).
Returns — self
Image.gamma()
imagine.Image.gamma(gamma)
Applies a gamma curve, moving the midtones without touching black or white.
Parameters
gamma(number) — Greater than zero. Below 1 darkens, above 1 lightens.
Returns — self
Image.levels()
imagine.Image.levels(black, white, gamma)
Stretches a range of input values across the full scale.
The single most useful correction for a flat or washed-out photograph:
everything at or below black becomes black, everything at or above
white becomes white, and the rest is spread between them.
Parameters
black(number) — The input value that becomes black, 0-255.white(number) — The input value that becomes white, 0-255.gamma(?number) — A midtone curve to apply along the way - Default: 1.
Returns — self
Image.invert()
imagine.Image.invert()
Inverts the colour channels, producing a photographic negative.
Alpha is untouched, so a transparent image stays transparent rather than becoming opaque.
Returns — self
Image.threshold()
imagine.Image.threshold(level)
Forces every pixel to black or white by comparing its brightness against a threshold.
The image is converted to grey first, so the split is made on perceived brightness rather than on each channel separately.
Parameters
level(?number) — The cut, 0-255 - Default: 128.
Returns — self
Image.posterize()
imagine.Image.posterize(levels)
Reduces each channel to a fixed number of evenly spaced steps.
This flattens gradients into visible bands. For a reduction that picks
the colours to keep rather than spacing them evenly, use quantize().
Parameters
levels(number) — 2 or more.
Returns — self
Image.opacity()
imagine.Image.opacity(factor)
Scales the alpha channel, making the whole image more transparent.
The scaling is relative: applying 0.5 twice leaves a quarter of the original opacity, not half.
Parameters
factor(number) — 0 (invisible) to 1 (unchanged).
Returns — self
Image.grayscale()
imagine.Image.grayscale()
Converts the image to shades of grey.
Channels are weighted for perceived brightness, so a bright yellow comes out light and a deep blue comes out dark, which is what the eye expects.
Returns — self
Image.sepia()
imagine.Image.sepia()
Gives the image the warm brown cast of an old photograph.
Returns — self
Image.saturate()
imagine.Image.saturate(amount)
Scales the image’s saturation.
Parameters
amount(number) — 0 removes all colour, 1 leaves it alone, above 1 intensifies.
Returns — self
Image.hue_rotate()
imagine.Image.hue_rotate(degrees)
Rotates every hue around the colour wheel, leaving brightness and saturation alone.
Parameters
degrees(number)
Returns — self
Image.duotone()
imagine.Image.duotone(shadow, highlight)
Maps the image’s brightness onto a two-colour ramp.
Dark pixels take shadow, light ones take highlight, and everything
between is interpolated.
Parameters
shadow(Color|string|number)highlight(Color|string|number)
Returns — self
Image.tint()
imagine.Image.tint(color, amount)
Blends a flat colour into the image.
At an amount of 1 the image becomes that colour entirely; at 0.2 it takes on a wash of it. Alpha is untouched, so transparent areas stay transparent.
Parameters
color(Color|string|number)amount(?number) — 0 to 1 - Default: 0.5.
Returns — self
Image.colorize()
imagine.Image.colorize(color)
Converts to grey and then tints, producing a monochrome image in a single colour.
Parameters
color(Color|string|number)
Returns — self
Image.quantize()
imagine.Image.quantize(colors, dither)
Reduces the image to at most a given number of colours.
Unlike posterize(), the colours kept are chosen to suit the image
rather than spaced evenly, so a photograph survives far fewer of them.
Dithering trades visible banding for a fine stipple, which almost always
looks better at low colour counts.
This is also a preview of what GIF export will do, since GIF cannot hold more than 256 colours per frame.
Parameters
colors(?number) — 2 to 256 - Default: 256.dither(?bool) — Default: false.
Returns — self
Image.premultiply()
imagine.Image.premultiply()
Multiplies the colour channels by alpha.
imagine keeps straight alpha everywhere, so this is for handing pixels
to something that expects them premultiplied. It loses precision in
near-transparent pixels and cannot be perfectly undone.
Returns — self
Image.unpremultiply()
imagine.Image.unpremultiply()
Undoes premultiply(), as far as 8 bits allow.
Returns — self
Image.convolve()
imagine.Image.convolve(kernel, options)
Applies a convolution kernel to the image.
The kernel is a flat list of size * size numbers with an odd size.
The default divisor of 0 means “divide by the kernel’s own sum”, which
is what nearly every published kernel expects; pass an explicit divisor
for the ones whose weights sum to zero.
Alpha goes through the kernel along with the colour channels unless
keep_alpha says otherwise, so blurring a shape softens its edge rather
than leaving a hard cutout. Turn that off for any kernel whose weights
do not sum to one: a Laplacian over a uniformly opaque image sums to
zero, and would make the whole result invisible.
Parameters
kernel(list)options(?dict) —divisor(default 0),offset(default 0),edge(defaultEDGE_CLAMP),keep_alpha(default false).
Returns — self
Image.blur()
imagine.Image.blur(radius)
Blurs the image with a true Gaussian.
The radius is the blur’s standard deviation in pixels: about two thirds of each pixel’s contribution falls within that distance, and the visible spread is roughly three times it. Cost grows with the radius but only linearly, because the blur is separable.
Alpha is premultiplied for the duration, so blurring a shape on a transparent background does not drag a dark halo into its edge.
Parameters
radius(?number) — Greater than zero - Default: 2.
Returns — self
Image.sharpen()
imagine.Image.sharpen(strength)
Sharpens the image.
Parameters
strength(?number) — 1 is moderate; much above 3 starts producing halos - Default: 1.
Returns — self
Image.smooth()
imagine.Image.smooth(weight)
Softens the image with a weighted 3x3 average.
Cheaper and harsher than blur(), and fixed at one pixel of reach.
Parameters
weight(?number) — How much of the original to keep - Default: 8.
Returns — self
Image.emboss()
imagine.Image.emboss()
Turns the image into a grey relief, as if stamped into metal.
Returns — self
Image.edges()
imagine.Image.edges()
Leaves only the image’s edges, everything flat going black.
Returns — self
Image.mean_removal()
imagine.Image.mean_removal()
Exaggerates local detail by removing the local mean.
Returns — self
Image.pixelate()
imagine.Image.pixelate(size)
Replaces each block of pixels with its average, producing the familiar censoring mosaic.
Parameters
size(?number) — The block’s side in pixels - Default: 8.
Returns — self
Image.draw_image()
imagine.Image.draw_image(source: instance, x, y, options)
Draws another image onto this one.
The source is clipped to this image’s edges, and its position may be negative, so a sprite hanging off the top-left corner draws correctly.
var badge = Image.open('badge.png')
photo.draw_image(badge, 12, 12, { opacity: 0.8 })
Parameters
source(Image)x(?number) — Default: 0.y(?number) — Default: 0.options(?dict) —opacity(0-1, default 1),blend(defaultBLEND_NORMAL).
Returns — self
Image.place()
imagine.Image.place(source: instance, anchor, options)
Draws another image onto this one, positioned by anchor rather than by coordinates.
photo.place(watermark, BOTTOM_RIGHT, { margin: 16, opacity: 0.5 })
Parameters
source(Image)anchor(?string) — Default:CENTER.options(?dict) —margin(inset from the edges, default 0), plus everythingdraw_image()takes.
Returns — self
Image.mask()
imagine.Image.mask(mask: instance)
Uses another image as an alpha mask.
Each of this image’s pixels keeps its colour and takes its transparency from the mask: where the mask is opaque white this image shows through fully, where the mask is black or transparent this image disappears, and greys give partial transparency.
The mask’s own alpha counts too, so a shape drawn on a transparent background works as a mask without being filled in first.
The mask must be the same size as this image.
Parameters
mask(Image)
Returns — self
Raises BoundsError When the mask is a different size.
Image.flatten()
imagine.Image.flatten(background)
Composites the image over a solid colour, leaving it fully opaque.
This is what happens on export to a format without an alpha channel; doing it explicitly first lets you choose the colour and see the result.
Parameters
background(?Color|string|number) — Default: white.
Returns — self
Image.histogram()
imagine.Image.histogram() -> dict
Counts how many pixels hold each value, per channel.
Returns a dictionary of four 256-entry lists: red, green, blue and
luma, where luma is perceived brightness. Fully transparent pixels are
skipped, since their colour is not visible and would otherwise pile up
at whatever the buffer happens to hold there.
A histogram is what tells you an image is underexposed (everything bunched at the low end), flat (bunched in the middle), or clipped (a spike at 0 or 255).
Returns dict
Image.auto_levels()
imagine.Image.auto_levels(options)
Stretches the image’s tones to fill the full range.
The black and white points are found from the brightness histogram,
ignoring a small fraction at each end so that a handful of stray pixels
cannot decide the result. This is the automatic version of levels(),
and it is the first thing to try on a flat or hazy photograph.
An image that already spans the full range, or one with no visible pixels at all, is left alone.
Parameters
options(?dict) —clip(the fraction to ignore at each end, 0 to 0.2 - Default: 0.005),gamma(a midtone curve to apply as well - Default: 1).
Returns — self
Image.average_color()
imagine.Image.average_color() -> Color
The average colour of the image.
Pixels are weighted by their alpha, so a mostly transparent image reports the colour of the part you can actually see rather than having it washed out by whatever is behind the transparency. The returned colour’s own alpha is the mean alpha.
An image with nothing visible in it returns transparent black.
Returns Color
Image.dominant_colors()
imagine.Image.dominant_colors(count, options) -> list
The colours that occupy the most of the image, most common first.
The image is shrunk and reduced to a small palette first, so this costs about the same whatever the original size. Nearly transparent pixels are ignored.
The usual use is picking a background or accent colour to show while a photograph is still loading.
var accent = photo.dominant_colors(1)[0]
Parameters
count(?number) — How many to return - Default: 5.options(?dict) —sample(the size the image is reduced to before counting - Default: 96),palette(how many colours to quantize to - Default: 32).
Returns list
Image.difference()
imagine.Image.difference(other: instance) -> number
How different two images are, from 0 (identical) to 1.
The score is the mean difference across the colour channels, weighted by how opaque each pixel is in either image, so transparent regions do not count as agreement. Images of different sizes are never comparable and return 1.
if rendered.difference(expected) > 0.01 {
raise Exception('the rendering changed')
}
Parameters
other(Image)
Returns number
Image.text()
imagine.Image.text(x, y, text, font, color, options)
Draws a string.
x and y are the top-left corner of the text’s box, not its baseline,
because that is what you know when placing text in a layout. Pass { baseline: true }
to treat y as the first line’s baseline instead.
A \n starts a new line. Multi-line text is aligned by the align
option, which positions the lines against each other, not against the
image.
var font = Font.load('Inter.ttf').size(28)
card.text(24, 24, 'Hello\nthere', font, '#111111', {
align: ALIGN_CENTER,
line_height: 1.4,
})
Parameters
x(number)y(number)text(string)font(Font)color(Color|string|number)options(?dict) —align,line_height(multiplier, default 1),tracking(extra pixels between characters, default 0),baseline(bool, default false),width(wrap to this many pixels).
Returns — self
Image.text_size()
imagine.Image.text_size(text, font, options) -> dict
Measures a string as text() would draw it, without drawing it.
Returns a dictionary holding width, height, baseline and lines.
Parameters
text(string)font(Font)options(?dict) — The same optionstext()takes,widthincluded, so a measurement of wrapped text matches what would actually be drawn.
Returns dict
Image.place_text()
imagine.Image.place_text(text, font, color, anchor, options)
Draws a string positioned by anchor rather than by coordinates.
Parameters
text(string)font(Font)color(Color|string|number)anchor(?string) — Default:CENTER.options(?dict) —margin, plus everythingtext()takes.
Returns — self
Image.encode()
imagine.Image.encode(format, options) -> bytes
Encodes the image and returns the bytes.
Options understood, by format:
quality(1-100) for JPEG and AVIF. Defaults to 85 for JPEG and 80 for AVIF. It does not apply to WebP, which is written losslessly.compressionfor PNG:'fast','default'or'best'. All three are lossless; they trade encoding time for file size.speedfor AVIF (1-10, lower is slower and smaller) and for GIF (1-30, lower is slower and picks better colours; 15 by default).backgroundfor JPEG, the colour transparent pixels are flattened against. Defaults to white.threshold(0-255) for WBMP, the brightness cut.
Parameters
format(?string) — Default: the format this image was decoded from, orPNG.options(?dict)
Returns bytes
Raises EncodeError When the format cannot be written.
Image.save()
imagine.Image.save(path: string, options)
Writes the image to a file.
The format is taken from the path’s extension unless format overrides
it, so save('out.webp') writes a WebP.
Parameters
path(string)options(?dict) —format, plus everythingencode()takes.
Returns — self
Raises FormatError When the extension names no format this module
knows and no format option is given.
Image.to_data_url()
imagine.Image.to_data_url(format, options) -> string
Encodes the image as a data: URL, ready to drop into an img tag or a
stylesheet.
Base64 costs a third more bytes than the raw image, so this suits icons and small graphics rather than photographs.
Parameters
format(?string) — Default:PNG.options(?dict) — Everythingencode()takes.
Returns string
Image.to_png()
imagine.Image.to_png(options) -> bytes
Encodes the image as a PNG. Lossless, with alpha.
Parameters
options(?dict) —compression.
Returns bytes
Image.to_jpeg()
imagine.Image.to_jpeg(quality, options) -> bytes
Encodes the image as a JPEG. Lossy, no alpha.
Parameters
quality(?number) — 1-100 - Default: 85.options(?dict) —background.
Returns bytes
Image.to_webp()
imagine.Image.to_webp(options) -> bytes
Encodes the image as a WebP.
Parameters
options(?dict)
Returns bytes
Image.to_avif()
imagine.Image.to_avif(quality, options) -> bytes
Encodes the image as an AVIF.
Small files, slow encoding. Lower speed values are slower and produce
smaller files.
Parameters
quality(?number) — 1-100 - Default: 80.options(?dict) —speed(1-10, default 6).
Returns bytes
Image.to_gif()
imagine.Image.to_gif(options) -> bytes
Encodes the image as a single-frame GIF, quantized to 256 colours.
Parameters
options(?dict)
Returns bytes
Image.to_bmp()
imagine.Image.to_bmp(options) -> bytes
Encodes the image as a BMP.
Parameters
options(?dict)
Returns bytes
Image.to_tiff()
imagine.Image.to_tiff(options) -> bytes
Encodes the image as a TIFF.
Parameters
options(?dict)
Returns bytes
Image.to_qoi()
imagine.Image.to_qoi(options) -> bytes
Encodes the image as a QOI. Lossless with alpha, and several times faster than PNG to read and write.
Parameters
options(?dict)
Returns bytes
Image.to_tga()
imagine.Image.to_tga(options) -> bytes
Encodes the image as a Targa.
Parameters
options(?dict)
Returns bytes
Image.to_ico()
imagine.Image.to_ico(options) -> bytes
Encodes the image as a Windows icon.
Parameters
options(?dict)
Returns bytes
Image.to_wbmp()
imagine.Image.to_wbmp(threshold) -> bytes
Encodes the image as a one-bit Wireless Bitmap.
Parameters
threshold(?number) — The brightness cut, 0-255 - Default: 128.
Returns bytes
Image.to_string()
imagine.Image.to_string()
2026, Richard Ore and Zuri contributors
imagine.strokefont
import imagine.strokefont
imaginelifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledimagine.strokefont.*needsimport imagine.strokefont.
StrokeFont: the font built into the module, defined as stroke geometry
rather than glyph outlines.
It exists so that text can be drawn with no font file present at all. Because a glyph is a path rather than a filled shape, it scales to any size and takes a weight without needing a separate face per weight.
Constants
EM
imagine.strokefont.EM: number = 20
The em square’s height in design units. A font asked for 20 pixels draws
its em at 20 pixels, so scale is simply size / EM.
CAP_HEIGHT
imagine.strokefont.CAP_HEIGHT: number = 14
The height of a capital letter.
X_HEIGHT
imagine.strokefont.X_HEIGHT: number = 10
The height of a lowercase letter with no ascender.
ASCENDER
imagine.strokefont.ASCENDER: number = 15
How far the tallest glyphs rise above the baseline.
DESCENDER
imagine.strokefont.DESCENDER: number
How far descenders fall below the baseline, as a negative number.
WEIGHT
imagine.strokefont.WEIGHT: number
The default stroke width in design units, a little under a tenth of the em, which is the usual weight for a regular sans-serif.
GLYPHS
imagine.strokefont.GLYPHS = {...}
Every glyph, keyed by character.
advance is how far the pen moves after drawing, in design units.
strokes is a list of polylines.
NOTDEF
imagine.strokefont.NOTDEF = {...}
What an unmapped character draws: an empty box, the same convention a font uses for a glyph it does not have.
Classes
StrokeFont
class imagine.StrokeFont
The built-in font: a sans-serif drawn from centre-line strokes rather than loaded from a file.
imagine deliberately ships no font file, so [[imagine.Font.system]]
and [[imagine.Font.sans]] both depend on what happens to be installed.
This one is always there. It is defined as geometry in
[[imagine.strokefont]], drawn through the same anti-aliased rasterizer
everything else uses, and therefore scales cleanly to any size and takes
a weight as a parameter.
import imagine { Image, StrokeFont }
Image(300, 80, 'white')
.text(16, 22, 'Always available', StrokeFont(28), '#0f172a')
.save('label.png')
It has the same interface as [[imagine.Font]], so anywhere a font is accepted this can be used instead.
What it is for
Labels, chart axes, watermarks, diagrams, placeholder text, and any output that has to work on a machine with no fonts installed. The look is geometric and single-weight, closer to a technical drawing than to a typeface, because that is what centre-line strokes give you honestly.
It is not a substitute for real typography. For body text, a headline, or anything where the shapes matter, load an actual font with [[imagine.Font.load]].
Coverage
Printable ASCII, from space through ~. Anything else draws the empty
box a font uses for a glyph it does not have, so text in another script
comes out visibly missing rather than silently blank.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
imagine.StrokeFont(size: ?number, weight: ?number)
Creates the built-in font at a size.
Parameters
size(?number) — The size in pixels - Default: 16.weight(?number) — The stroke width, as a fraction of the size - Default: 0.085, a regular weight. Around 0.13 reads as bold.
StrokeFont.size()
imagine.StrokeFont.size(pixels) -> StrokeFont
Returns the same font at a different size.
Parameters
pixels(number)
Returns StrokeFont
StrokeFont.weight()
imagine.StrokeFont.weight(weight) -> StrokeFont
Returns the same font at a different stroke weight.
Parameters
weight(number) — A fraction of the size. 0.085 is regular, 0.13 reads as bold, 0.05 as light.
Returns StrokeFont
StrokeFont.pixel_size()
imagine.StrokeFont.pixel_size() -> number
The size in pixels this font draws at.
Returns number
StrokeFont.name()
imagine.StrokeFont.name() -> string
The font’s name.
Returns string
StrokeFont.source()
imagine.StrokeFont.source() -> string
Where this font came from.
Returns string
StrokeFont.has_glyph()
imagine.StrokeFont.has_glyph(character) -> bool
true when this font has a glyph for a character, which for the
built-in font means printable ASCII.
Parameters
character(string)
Returns bool
StrokeFont.metrics()
imagine.StrokeFont.metrics() -> dict
The font’s vertical metrics at its current size, in pixels.
The dictionary holds name, ascent, descent (negative), line_gap
and line_height, the same shape [[imagine.Font]] reports. The em is
the size, so single-spaced lines sit exactly size pixels apart.
Returns dict
StrokeFont.wrap()
imagine.StrokeFont.wrap(text, width: number, options) -> string
Breaks a string into lines that fit within a width, returning the text with newlines inserted.
Parameters
text(string)width(number) — The maximum line width in pixels.options(?dict) —tracking.
Returns string
StrokeFont.measure()
imagine.StrokeFont.measure(text, options) -> dict
Measures a string without drawing it.
Returns width, height, baseline (the first line’s baseline,
measured down from the top of the box) and lines.
Parameters
text(string)options(?dict) —tracking,line_height,align.
Returns dict
StrokeFont.render()
imagine.StrokeFont.render(text, options) -> dict
Rasterizes a string into an 8-bit coverage mask.
Returns coverage, width, height and baseline, the same shape
[[imagine.Font]] returns, which is what lets both be handed to
Image.text() interchangeably.
Parameters
text(string)options(?dict) —tracking,line_height,align.
Returns dict
StrokeFont.to_string()
imagine.StrokeFont.to_string()
2026, Richard Ore and Zuri contributors
sql
import sql
One way to talk to a relational database, whichever one it is.
sql is the entry point for every database Zuri supports. It defines
what a database adapter has to provide, supplies everything that is the
same whichever engine answers, and picks the adapter from the connection
string. Changing database means changing that string, and whatever SQL
the engines genuinely spell differently.
import sql
var db = sql.open(':memory:')
db.exec('create table posts (id integer primary key, title text)')
var id = db.insert('posts', { title: 'Hello' })
for post in db.query('select * from posts where id = ?', [id]) {
echo post.title
}
db.close()
The only line above that names an engine is the first. Point it at
postgres://localhost/app or mysql://localhost/app and the rest runs
unchanged: the ? becomes $1 on PostgreSQL and stays ? on MySQL,
the insert gets a RETURNING clause where the engine has no last insert
id, and a unique key violation still arrives as UniqueViolation.
What is here
open() | opens a connection from a connection string |
pool() | opens a pool of them |
register() | adds an adapter of your own |
Connection | what a program holds and runs statements on |
Transaction | an open transaction, and the savepoints inside it |
Statement | a statement compiled once and run many times |
Cursor | a result read a row at a time |
ResultSet | the rows a query returned |
Decimal | an exact decimal, for a column a float must not hold |
Time | a signed span of time, as a TIME column holds one |
Schema | what tables exist and what is in them |
SqlError | the root of every error a database raises |
Parameters
Values are bound, never pasted into the statement. Write ? for
positional parameters or :name for named ones and sql translates
them into whatever the adapter wants:
db.query('select * from posts where author = ? and year = ?', [name, 2026])
db.query('select * from posts where author = :who', { who: name })
Errors
Every error is a SqlError. The subclasses are the same on every
engine, so the code that handles a duplicate key does not change when
the engine does:
catch {
db.insert('users', { email: address })
} as error {
if instance_of(error, sql.UniqueViolation) {
echo 'that address is already registered'
} else {
raise error
}
}
Isolates
A connection belongs to the isolate that opened it and cannot be shared with another. An isolate that needs a database opens its own connection or its own pool.
The sql API
Every public name in sql, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
sql.AuthenticationError | class | The server rejected the credentials. |
sql.CheckViolation | class | A CHECK constraint rejected the row. |
sql.ClosedError | class | The connection, statement, cursor, transaction or pool was already closed. |
sql.Connection | class | An open connection to a database. |
sql.ConnectionError | class | The database could not be reached, or the connection dropped. |
sql.Cursor | class | A result being read a row at a time. |
sql.DeadlockError | class | Two transactions were each waiting on the other, and the engine broke the tie by aborting this one. |
sql.Decimal | class | An exact decimal number. |
sql.Driver | class | Opens connections to one kind of database. |
sql.DriverConnection | class | One open connection to a database, as the layer above it sees one. |
sql.DriverCursor | class | A result being read a batch at a time rather than all at once. |
sql.DriverStatement | class | A statement compiled once and run many times. |
sql.ExecResult | class | What a statement that changed something reports. |
sql.ForeignKeyViolation | class | A foreign key has no matching row, or a referenced row is still in use by another table. |
sql.INDEXED | constant | ?1, ?2 and so on. |
sql.IntegrityError | class | A constraint rejected the data. |
sql.NUMBERED | constant | $1, $2 and so on, numbered from one in binding order. |
sql.NotNullViolation | class | A column declared NOT NULL was given nothing. |
sql.NotSupportedError | class | The engine cannot do what was asked, and no approximation would be honest. |
sql.Pool | class | A pool of connections to one database. |
sql.PoolError | class | Something went wrong with a connection pool rather than with a database. |
sql.PoolExhaustedError | class | Every connection was busy and none came free within the acquire timeout. |
sql.ProtocolError | class | The server sent something the adapter could not make sense of. |
sql.QUESTION | constant | A bare ? for every parameter, in order. |
sql.QueryError | class | The engine refused the statement. |
sql.READ_COMMITTED | constant | Each transaction sees rows committed before its own statement started, and nothing a concurrent transaction… |
sql.READ_UNCOMMITTED | constant | A transaction can see rows another has written and not committed. |
sql.REPEATABLE_READ | constant | A transaction reads the same rows throughout, whatever anyone else commits while it runs. |
sql.Raw | class | Marks a fragment of SQL to be used as written rather than bound. |
sql.ResultSet | class | The rows a query returned, with the shape of the result beside them. |
sql.SERIALIZABLE | constant | Concurrent transactions produce a result some serial order of them could also have produced. |
sql.Schema | class | Introspection for one connection. |
sql.SchemaAdapter | class | The introspection queries for one engine. |
sql.SerializationError | class | Two transactions could not both be treated as though they ran alone. |
sql.SqlError | class | Base class for every error this module and its adapters raise. |
sql.Statement | class | A prepared statement. |
sql.Time | class | A signed span of time, as a SQL TIME column holds one. |
sql.TimeoutError | class | An operation ran out of time. |
sql.Transaction | class | An open transaction. |
sql.TransactionError | class | A transaction could not be carried out as asked. |
sql.UniqueViolation | class | A unique index or primary key already holds this value. |
sql.column_shape | function | The description of one column, as every adapter returns it. |
sql.crud.delete | function | Builds a DELETE. |
sql.crud.insert | function | Builds an INSERT. |
sql.crud.insert_many | function | Builds an INSERT carrying several rows in one statement. |
sql.crud.quote_table | function | Renders a table name, which may carry a schema. |
sql.crud.select | function | Builds a SELECT. |
sql.crud.update | function | Builds an UPDATE. |
sql.crud.where | function | Builds the WHERE clause for a dictionary of conditions. |
sql.decimal | function | Builds a decimal, the same as Decimal() but as a function. |
sql.default_capabilities | function | The capability flags a driver reports, with the value each takes when a driver does not say otherwise. |
sql.driver | function | The adapter registered under name. |
sql.driver_for | function | The adapter a connection string names. |
sql.drivers | function | The names of every registered adapter. |
sql.mysql.CACHING_SHA2 | constant | The default since MySQL 8.0. |
sql.mysql.CAPABILITIES | constant | What the client asks for and what the server offers. |
sql.mysql.CLEAR | constant | Sends the password as it is. |
sql.mysql.COMMANDS | constant | The commands this adapter sends. |
sql.mysql.DEFAULT_PORT | constant | The port a MySQL server listens on unless told otherwise. |
sql.mysql.DRIVER | constant | The driver instance sql registers for MySQL. |
sql.mysql.ED25519 | constant | MariaDB’s signature-based plugin. |
sql.mysql.FLAGS | constant | What the server says about a column beyond its type. |
sql.mysql.MARIADB_DRIVER | constant | The driver instance sql registers for MariaDB. |
sql.mysql.MAX_PARAMETERS | constant | Most parameters one statement can bind. |
sql.mysql.MariaDbDriver | class | Opens MariaDB connections. |
sql.mysql.MySqlConnection | class | A connection to MySQL or MariaDB. |
sql.mysql.MySqlCursor | class | A result being read in batches. |
sql.mysql.MySqlDriver | class | Opens MySQL connections. |
sql.mysql.MySqlStatement | class | A prepared statement. |
sql.mysql.NATIVE | constant | MySQL’s original plugin, and still MariaDB’s default. |
sql.mysql.SHA256 | constant | MySQL 5.7’s stronger option, superseded by the caching one. |
sql.mysql.SSL_MODES | constant | What sslmode can be. |
sql.mysql.TYPES | constant | The type byte the server puts on a column, and that a parameter carries back. |
sql.mysql.auth.NONE | constant | Named in a handshake by a server that wants no password at all. |
sql.mysql.auth.SUPPORTED | constant | The plugins this adapter can answer. |
sql.mysql.auth.caching_sha2_password | function | The fast caching_sha2_password response. |
sql.mysql.auth.clear_password | function | The password as the server reads it when it is sent outright: the bytes and the zero that ends them. |
sql.mysql.auth.ed25519_password | function | The client_ed25519 response, which MariaDB uses. |
sql.mysql.auth.encrypt_password | function | The password encrypted to the server’s public key, for the plugins that need the password itself over a… |
sql.mysql.auth.native_password | function | The mysql_native_password response. |
sql.mysql.auth.obfuscate | function | The password masked with the scramble, which is the form the two RSA plugins encrypt. |
sql.mysql.auth.response_for | function | The first response to send for plugin, before any back and forth. |
sql.mysql.auth.supports | function | Whether plugin is one this adapter knows how to answer. |
sql.mysql.class_for | function | The class that best describes an error the server reported. |
sql.mysql.connect | function | Opens a MySQL connection directly, without going through sql. |
sql.mysql.connection.CACHE_LIMIT | constant | How many compiled statements a connection keeps before releasing the one it has gone longest without using. |
sql.mysql.driver.DEFAULT_TIMEOUT | constant | How long to wait for the socket and for each read, in milliseconds. |
sql.mysql.ed25519.D | constant | The curve constant, -121665/121666. |
sql.mysql.ed25519.D2 | constant | Twice the curve constant, which is what the addition uses. |
sql.mysql.ed25519.L | constant | The order of the base point’s subgroup. |
sql.mysql.ed25519.P | constant | The field prime, 2^255 - 19. |
sql.mysql.ed25519.password_key | function | Expands a password into the 64 bytes MariaDB signs with. |
sql.mysql.ed25519.public_key | function | The public key matching an expanded key. |
sql.mysql.ed25519.sign | function | Signs message with an expanded key. |
sql.mysql.errors.CLASSES | constant | SQLSTATE classes, for numbers the table above does not name. |
sql.mysql.errors.SPECIFIC | constant | Error numbers specific enough to name a class of their own. |
sql.mysql.packets.COMPRESS_THRESHOLD | constant | Below this, a compressed packet is sent uncompressed instead. |
sql.mysql.packets.Cursor | class | Reads values out of one packet payload, keeping its own position. |
sql.mysql.packets.EXACT_INTEGER_LIMIT | constant | Largest integer a Zuri number holds exactly. |
sql.mysql.packets.LENENC_NULL | constant | The first byte of a length-encoded integer that means the value is NULL. |
sql.mysql.packets.MAX_PAYLOAD | constant | The largest payload a single packet can carry. |
sql.mysql.packets.Writer | class | Builds one packet payload. |
sql.mysql.protocol.CURSOR_NONE | constant | No cursor: the whole result comes back at once. |
sql.mysql.protocol.CURSOR_READ_ONLY | constant | Asks the server to keep the result open rather than send it all. |
sql.mysql.protocol.STATUS | constant | What the server says about the session after each command. |
sql.mysql.raise_for | function | Raises the right class for a server error report. |
sql.mysql.results.exec_result | function | What a statement run for its effect reports. |
sql.mysql.results.select_result | function | What a statement run for its rows reports. |
sql.mysql.schema.MySqlSchema | class | Answers introspection questions about a MySQL database. |
sql.mysql.type_name_of | function | The name of a column’s type, as MySQL itself would write it. |
sql.mysql.types.BINARY_CHARSET | constant | The collation id that means a column holds bytes rather than text. |
sql.mysql.types.UNSIGNED_FLAG | constant | The flag byte that goes with a parameter’s type to mark it unsigned. |
sql.mysql.types.decode_binary | function | Reads one value out of a binary protocol row. |
sql.mysql.types.decode_text | function | Reads one value out of a text protocol row. |
sql.mysql.types.describe_columns | function | Reduces the server’s column descriptors to the two fields the shared contract asks for. |
sql.mysql.types.is_binary | function | Whether a column holds bytes rather than text. |
sql.mysql.types.is_unsigned | function | Whether a column’s values are unsigned. |
sql.mysql.types.parameter_type | function | The type byte and flag a value is sent as. |
sql.mysql.types.write_parameter | function | Appends a value in the form its type byte promises. |
sql.open | function | Opens a connection. |
sql.params.STYLES | constant | Every style a driver may declare. |
sql.params.tokenize | function | Splits sql into the pieces that matter, in order. |
sql.parse_time | function | Reads a SQL TIME literal. |
sql.placeholder | function | The placeholder text for the parameter at position, counting from one. |
sql.pool | function | Opens a pool of connections. |
sql.pool.DEFAULT_IDLE_TIMEOUT | constant | How long an idle connection is kept before being closed, in milliseconds. |
sql.pool.DEFAULT_MAX | constant | How many connections a pool opens at most, when it is not told. |
sql.pool.DEFAULT_MAX_LIFETIME | constant | How long any connection is kept before being replaced, in milliseconds. |
sql.postgres.DEFAULT_PORT | constant | The port a PostgreSQL server listens on unless told otherwise. |
sql.postgres.DEFAULT_REGISTRY | constant | The registry every connection uses unless given one of its own. |
sql.postgres.DRIVER | constant | The driver instance sql registers for this engine. |
sql.postgres.Listener | class | Subscribes a connection to channels and collects what arrives. |
sql.postgres.MAX_PARAMETERS | constant | Most parameters one statement can bind. |
sql.postgres.OIDS | constant | PostgreSQL’s built-in type OIDs, by name. |
sql.postgres.PostgresConnection | class | An open PostgreSQL connection. |
sql.postgres.PostgresCursor | class | A portal being read. |
sql.postgres.PostgresDriver | class | Opens PostgreSQL connections. |
sql.postgres.PostgresStatement | class | A compiled PostgreSQL statement. |
sql.postgres.SSL_MODES | constant | What sslmode can be. |
sql.postgres.TypeRegistry | class | Which decoder and encoder each OID uses. |
sql.postgres.affected_rows | function | The number of rows a command tag reports. |
sql.postgres.auth.SCRAM_SHA_256 | constant | The mechanism this adapter implements. |
sql.postgres.auth.client_first | function | The client’s opening SCRAM message. |
sql.postgres.auth.client_proof | function | Works out the client’s proof from the server’s challenge. |
sql.postgres.auth.md5_response | function | Builds the response to an MD5 password request. |
sql.postgres.auth.nonce | function | A fresh SCRAM nonce. |
sql.postgres.auth.offers_scram | function | Whether the server offered SCRAM-SHA-256 among its mechanisms. |
sql.postgres.auth.parse_scram | function | Splits a SCRAM message into its key=value parts. |
sql.postgres.auth.verify_server | function | Checks the server’s closing message really is from a server that knows the password. |
sql.postgres.class_for | function | The class that best describes sqlstate. |
sql.postgres.connect | function | Opens a PostgreSQL connection directly, without going through sql. |
sql.postgres.connection.CACHE_LIMIT | constant | How many compiled statements a connection keeps. |
sql.postgres.describe_columns | function | Reduces the server’s column descriptors to the two fields the shared contract asks for. |
sql.postgres.driver.DEFAULT_TIMEOUT | constant | How long to wait for the socket and for each read, in milliseconds. |
sql.postgres.errors.CLASSES | constant | SQLSTATE classes, for codes the table above does not name. |
sql.postgres.errors.SPECIFIC | constant | Codes specific enough to name a class of their own. |
sql.postgres.messages.AUTHENTICATION | constant | Messages the server sends, by tag. |
sql.postgres.messages.AUTH_CLEARTEXT | constant | |
sql.postgres.messages.AUTH_MD5 | constant | |
sql.postgres.messages.AUTH_OK | constant | Authentication requests, by the number that follows the tag. |
sql.postgres.messages.AUTH_SASL | constant | |
sql.postgres.messages.AUTH_SASL_CONTINUE | constant | |
sql.postgres.messages.AUTH_SASL_FINAL | constant | |
sql.postgres.messages.BACKEND_KEY_DATA | constant | |
sql.postgres.messages.BIND_COMPLETE | constant | |
sql.postgres.messages.CLOSE_COMPLETE | constant | |
sql.postgres.messages.COMMAND_COMPLETE | constant | |
sql.postgres.messages.COPY_DATA | constant | |
sql.postgres.messages.COPY_DONE | constant | |
sql.postgres.messages.COPY_IN_RESPONSE | constant | |
sql.postgres.messages.COPY_OUT_RESPONSE | constant | |
sql.postgres.messages.DATA_ROW | constant | |
sql.postgres.messages.EMPTY_QUERY_RESPONSE | constant | |
sql.postgres.messages.ERROR_RESPONSE | constant | |
sql.postgres.messages.NOTICE_RESPONSE | constant | |
sql.postgres.messages.NOTIFICATION_RESPONSE | constant | |
sql.postgres.messages.NO_DATA | constant | |
sql.postgres.messages.PARAMETER_DESCRIPTION | constant | |
sql.postgres.messages.PARAMETER_STATUS | constant | |
sql.postgres.messages.PARSE_COMPLETE | constant | |
sql.postgres.messages.PORTAL_SUSPENDED | constant | |
sql.postgres.messages.PROTOCOL_VERSION | constant | The protocol version this adapter speaks: 3.0, as a single number with the major version in the high half. |
sql.postgres.messages.READY_FOR_QUERY | constant | |
sql.postgres.messages.ROW_DESCRIPTION | constant | |
sql.postgres.messages.Reader | class | Reads framed messages from a connected stream. |
sql.postgres.messages.SSL_REQUEST | constant | The number the server recognises as a request to start TLS. |
sql.postgres.messages.Writer | class | Builds one outgoing message. |
sql.postgres.messages.read_cstring | function | Splits a run of zero-terminated strings. |
sql.postgres.raise_for | function | Raises the right class for a server error report. |
sql.postgres.schema.PostgresSchema | class | Answers introspection questions about a PostgreSQL database. |
sql.postgres.type_name_of | function | The name of the type an OID stands for, or the OID as text for one this adapter has no name for, which is… |
sql.postgres.types.ARRAY_ELEMENTS | constant | The element type each array OID holds. |
sql.postgres.types.EPOCH_OFFSET | constant | Seconds between the Unix epoch and PostgreSQL’s, which is 2000-01-01 rather than 1970-01-01. |
sql.postgres.types.date_from_micros | function | Turns a count of microseconds since the PostgreSQL epoch into a date.Date in UTC. |
sql.postgres.types.decode_array | function | Reads an array back, however many dimensions it has. |
sql.postgres.types.decode_bool | function | A decoder reads one column value from its binary form. |
sql.postgres.types.decode_box | function | |
sql.postgres.types.decode_bytea | function | |
sql.postgres.types.decode_circle | function | |
sql.postgres.types.decode_date | function | date is a count of days from 2000-01-01, with no time part. |
sql.postgres.types.decode_float4 | function | |
sql.postgres.types.decode_float8 | function | |
sql.postgres.types.decode_inet | function | inet and cidr share a form: address family, prefix bits, a flag saying which of the two it is, then the… |
sql.postgres.types.decode_int2 | function | |
sql.postgres.types.decode_int4 | function | |
sql.postgres.types.decode_int8 | function | |
sql.postgres.types.decode_interval | function | interval is a span rather than a point, so it comes back as its parts. |
sql.postgres.types.decode_json | function | |
sql.postgres.types.decode_jsonb | function | jsonb carries a version byte in front of the text, which every server so far sets to 1. |
sql.postgres.types.decode_lseg | function | |
sql.postgres.types.decode_macaddr | function | |
sql.postgres.types.decode_numeric | function | Decodes PostgreSQL’s numeric into an exact Decimal. |
sql.postgres.types.decode_numeric_value | function | |
sql.postgres.types.decode_oid | function | |
sql.postgres.types.decode_point | function | |
sql.postgres.types.decode_text | function | |
sql.postgres.types.decode_time | function | time is microseconds since midnight, with no date part. |
sql.postgres.types.decode_timestamp | function | Both timestamp and timestamptz are microseconds from the PostgreSQL epoch. |
sql.postgres.types.decode_timetz | function | timetz is a time followed by its offset in seconds west of UTC. |
sql.postgres.types.decode_uuid | function | |
sql.postgres.types.encode_array | function | Builds the binary form of an array. |
sql.postgres.types.encode_bool | function | An encoder turns a Zuri value into the bytes of its binary form. |
sql.postgres.types.encode_bytea | function | |
sql.postgres.types.encode_date | function | |
sql.postgres.types.encode_float4 | function | |
sql.postgres.types.encode_float8 | function | |
sql.postgres.types.encode_int2 | function | |
sql.postgres.types.encode_int4 | function | |
sql.postgres.types.encode_int8 | function | |
sql.postgres.types.encode_json | function | |
sql.postgres.types.encode_jsonb | function | jsonb takes the same text behind the version byte the server expects. |
sql.postgres.types.encode_numeric | function | Encodes a Decimal, a number or a bigint as numeric. |
sql.postgres.types.encode_text | function | |
sql.postgres.types.encode_timestamp | function | |
sql.postgres.types.encode_uuid | function | |
sql.postgres.types.micros_from_date | function | The microseconds since the PostgreSQL epoch a date.Date stands for. |
sql.postgres.types.read_int16 | function | Reads a signed 16 bit big-endian integer. |
sql.postgres.types.read_int32 | function | Reads a signed 32 bit big-endian integer. |
sql.postgres.types.read_int64 | function | Reads a signed 64 bit big-endian integer. |
sql.postgres.types.read_uint16 | function | Reads an unsigned 16 bit big-endian integer. |
sql.postgres.types.read_uint32 | function | Reads an unsigned 32 bit big-endian integer. |
sql.postgres.types.text_of | function | A bytes slice as a UTF-8 string. |
sql.postgres.types.write_int16 | function | Writes a signed 16 bit big-endian integer. |
sql.postgres.types.write_int32 | function | Writes a signed 32 bit big-endian integer. |
sql.postgres.types.write_int64 | function | Writes a signed 64 bit big-endian integer. |
sql.raw | function | Builds a Raw. |
sql.register | function | Registers an adapter, so open() recognises its connection strings. |
sql.scan | function | What placeholders sql uses, without needing any values. |
sql.split_statements | function | Splits a script into its statements. |
sql.sqlite.Backup | class | A copy in progress between two connections. |
sql.sqlite.Blob | class | An open blob handle. |
sql.sqlite.DEFAULT_BUSY_TIMEOUT | constant | How long a statement waits for another writer by default. |
sql.sqlite.DRIVER | constant | The driver instance sql registers for this engine. |
sql.sqlite.MAX_PARAMETERS | constant | Most parameters one statement can bind, which is SQLite’s own limit. |
sql.sqlite.MEMORY | constant | The path that means a private database held in memory, which is discarded when the connection closes. |
sql.sqlite.SqliteConnection | class | An open SQLite database. |
sql.sqlite.SqliteCursor | class | A result being stepped. |
sql.sqlite.SqliteDriver | class | Opens SQLite databases. |
sql.sqlite.SqliteStatement | class | A prepared SQLite statement. |
sql.sqlite.backup.DEFAULT_PAGES | constant | How many pages a step copies when no size is given. |
sql.sqlite.base_type | function | The bare type name from a declaration, upper cased and without any size or precision. |
sql.sqlite.class_for | function | The class that best describes code. |
sql.sqlite.connect | function | Opens a SQLite database directly, without going through sql. |
sql.sqlite.connection.CACHE_LIMIT | constant | How many compiled statements a connection keeps for reuse. |
sql.sqlite.conversion_for | function | Which conversion a column’s declaration calls for: 'bool', 'date', 'json', or nil for a column that… |
sql.sqlite.driver.OPEN_CREATE | constant | |
sql.sqlite.driver.OPEN_FULLMUTEX | constant | |
sql.sqlite.driver.OPEN_MEMORY | constant | |
sql.sqlite.driver.OPEN_NOMUTEX | constant | |
sql.sqlite.driver.OPEN_PRIVATECACHE | constant | |
sql.sqlite.driver.OPEN_READONLY | constant | |
sql.sqlite.driver.OPEN_READWRITE | constant | |
sql.sqlite.driver.OPEN_SHAREDCACHE | constant | |
sql.sqlite.driver.OPEN_URI | constant | |
sql.sqlite.errors.primary | function | An extended code’s primary code, which is its low byte. |
sql.sqlite.reraise | function | Re-raises an error from the native layer as the right class. |
sql.sqlite.schema.SqliteSchema | class | Answers introspection questions about a SQLite database. |
sql.sqlite.split_message | function | Splits the [code] message form the native layer raises. |
sql.sqlite.statement.bind_all | function | Binds values to a reset statement, in order. |
sql.sqlite.statement.describe | function | Describes the columns a compiled statement returns. |
sql.sqlite.types.BOOLEAN_TYPES | constant | Declared types that mean a boolean. |
sql.sqlite.types.JSON_TYPES | constant | Declared types that mean JSON held in a text column. |
sql.sqlite.types.TEMPORAL_TYPES | constant | Declared types that mean a point in time. |
sql.sqlite.types.conversions_for | function | The conversions for a whole result, one per column, worked out once so each row does not have to look at the… |
sql.sqlite.types.decode | function | Applies one column’s conversion to one stored value. |
sql.sqlite.types.decode_row | function | Applies a result’s conversions to one row of stored values. |
sql.time | function | Builds a Time. |
sql.time_from_seconds | function | Builds a Time of total seconds, splitting it into parts. |
sql.translate | function | Rewrites sql into style and puts the values in binding order. |
sql.types.ISO_DATE_FORMAT | constant | The same, without the time, for a column that holds only a date. |
sql.types.ISO_FORMAT | constant | The format this module writes timestamps in: ISO 8601 with microsecond precision and an explicit UTC offset. |
sql.types.flatten | function | Reduces value to something an engine with no richer type can store: a timestamp becomes ISO text, a list or… |
sql.types.from_iso | function | Reads ISO 8601 text back into a date.Date. |
sql.types.from_json | function | Decodes JSON text, returning nil for anything that will not parse. |
sql.types.integer_from_text | function | Reads an integer, however long, as a number where one holds it exactly and a bigint where it does not. |
sql.types.is_interval | function | Whether value is a Time. |
sql.types.is_structured | function | Whether value is something this module stores as JSON. |
sql.types.is_temporal | function | Whether value is a date.Date. |
sql.types.to_iso | function | Formats a date.Date as ISO 8601 text. |
sql.types.to_json | function | Encodes value as JSON text. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
sql.connection | sql.connection.* | The connection a program holds. |
sql.crud | sql.crud.* | Building the four statements that are the same everywhere. |
sql.cursor | sql.cursor.* | Reading a result without holding all of it. |
sql.decimal | sql.decimal.* | Exact decimal numbers, for the money column that must not be a float. |
sql.driver | sql.driver.* | The contract every database adapter implements, and the capability flags that let one adapter differ from… |
sql.errors | sql.* | Every error a database raises, under one root, in one shape, whatever engine it came from. |
sql.mysql | import sql.mysql | The MySQL and MariaDB adapter. |
sql.params | sql.params.* | Rewriting a statement’s placeholders into whatever the active adapter expects. |
sql.pool | sql.pool.* | Keeping connections open and lending them out. |
sql.postgres | import sql.postgres | The PostgreSQL adapter. |
sql.result | sql.result.* | What a statement hands back: the rows of a query, or the count of a statement that changed something. |
sql.schema | sql.schema.* | Asking a database what is in it. |
sql.sqlite | import sql.sqlite | The SQLite adapter. |
sql.statement | sql.statement.* | A statement compiled once and run many times. |
sql.transaction | sql.transaction.* | Transactions, and the nesting that savepoints make possible. |
sql.types | sql.types.* | How Zuri values and database values correspond, for the parts that are the same whichever engine is… |
Functions
register()
sql.register(driver) -> Driver
Registers an adapter, so open() recognises its connection strings.
An adapter is anything implementing Driver. Registering one under a
name already taken replaces it, which is how a program swaps in an
adapter of its own for an engine sql already knows.
sql.register(MyOracleDriver())
var db = sql.open('oracle://localhost/app')
Parameters
driver(Driver)
Returns Driver — The driver, so a registration can be an
expression.
Raises SqlError if driver is not one.
drivers()
sql.drivers() -> list[string]
The names of every registered adapter.
Returns list[string]
driver()
sql.driver(name: string) -> Driver
The adapter registered under name.
Parameters
name(string)
Returns Driver
Raises SqlError if nothing is registered under that name.
driver_for()
sql.driver_for(dsn: string) -> Driver
The adapter a connection string names.
The scheme decides: sqlite:// and postgres:// reach the adapters of
those names, and a string with no scheme at all is taken as a SQLite
path, which is the only form that could not be anything else.
Parameters
dsn(string)
Returns Driver
Raises SqlError if no adapter claims the scheme.
open()
sql.open(dsn, options) -> Connection
Opens a connection.
sql.open('sqlite://./app.db')
sql.open('postgres://alice:secret@localhost/app')
sql.open('./app.db')
An options dictionary works too, and needs driver to say which adapter
it is for:
sql.open({ driver: 'sqlite', path: './app.db', journal_mode: 'wal' })
Parameters
dsn(string|dict) — A connection string, or options.options(dict|nil) — Extra options, merged over whatever the connection string carried. Anything an adapter accepts in its own options goes here.
Returns Connection
Raises ConnectionError if the database cannot be reached.
pool()
sql.pool(dsn, options) -> Pool
Opens a pool of connections.
Parameters
dsn(string|dict) — Asopen()takes.options(dict|nil) — The pool’s own settings, and any the adapter accepts. SeePoolfor what the pool reads.
Returns Pool
2026, Richard Ore and Zuri contributors
sql.connection
import sql.connection
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.connection.*needsimport sql.connection.
The connection a program holds.
Everything that is the same whichever database is underneath lives here: translating placeholders, shaping results, running transactions and their savepoints, generating the four statements that are always the same, and getting an id back from an insert on an engine that has no last insert id.
What is left over is the adapter’s, reachable through native() for the
cases where an engine’s own feature is the point.
Classes
Connection
class sql.Connection
An open connection to a database.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
schema | Schema | Introspection: what tables exist, what columns they have, and how they are indexed. |
Constructor
sql.Connection(connection, driver)
Parameters
connection(DriverConnection) — The adapter’s own connection.driver(Driver) — The driver that opened it.
Connection.driver_connection()
sql.Connection.driver_connection() -> DriverConnection
The adapter’s own connection, for the engine-specific things the shared contract does not cover.
Returns DriverConnection
Connection.native()
sql.Connection.native() -> DriverConnection
The same thing, under the name that reads better at a call site.
db.native().create_function('slug', 1, @(text) => slugify(text))
Returns DriverConnection
Connection.driver()
sql.Connection.driver() -> Driver
The driver behind this connection.
Returns Driver
Connection.driver_name()
sql.Connection.driver_name() -> string
The adapter’s name, such as 'sqlite'.
Returns string
Connection.capabilities()
sql.Connection.capabilities() -> dict
What this engine can do.
Returns dict
Connection.supports()
sql.Connection.supports(flag: string) -> bool
Whether this engine supports flag.
if db.supports('returning') {
...
}
Parameters
flag(string)
Returns bool
Connection.server_version()
sql.Connection.server_version() -> string
The engine’s version, as it reports it.
Returns string
Connection.query()
sql.Connection.query(sql: string, params) -> ResultSet
Runs a query and reads its whole result.
Parameters
sql(string) — Written with?or:nameplaceholders.params(list|dict|nil)
Returns ResultSet
Connection.exec()
sql.Connection.exec(sql: string, params) -> ExecResult
Runs a statement for its effect.
Parameters
sql(string)params(list|dict|nil)
Returns ExecResult
Connection.fetch_one()
sql.Connection.fetch_one(sql: string, params) -> dict|nil
The first row of a query, or nil when it returns none.
Parameters
sql(string)params(list|dict|nil)
Returns dict|nil
Connection.fetch_all()
sql.Connection.fetch_all(sql: string, params) -> list[dict]
Every row of a query.
Parameters
sql(string)params(list|dict|nil)
Returns list[dict]
Connection.fetch_value()
sql.Connection.fetch_value(sql: string, params, fallback) -> any
The first column of the first row, for a query written to return one value.
Parameters
sql(string)params(list|dict|nil)fallback(any) — What to return for an empty result.
Returns any
Connection.fetch_column()
sql.Connection.fetch_column(sql: string, params, column) -> list
One column’s values, as a list.
Parameters
sql(string)params(list|dict|nil)column(string|number|nil) — Which column, the first by default.
Returns list
Connection.stream()
sql.Connection.stream(sql: string, params, options) -> Cursor
Runs a query and returns a cursor over its result rather than reading all of it.
Parameters
sql(string)params(list|dict|nil)options(dict|nil) —{ batch }on engines that fetch in batches.
Returns Cursor
Connection.prepare()
sql.Connection.prepare(sql: string) -> Statement
Compiles a statement for repeated use.
Parameters
sql(string)
Returns Statement
Connection.exec_script()
sql.Connection.exec_script(sql: string)
Runs one or more statements for their effect, as a script.
This is the path for a schema file or a batch of pragmas. It binds no parameters and returns no rows, and an engine that cannot run several statements at once runs them one at a time.
Parameters
sql(string)
Connection.insert()
sql.Connection.insert(table: string, values: dict, options) -> any
Inserts a row and returns its id.
How the id comes back depends on the engine, which is the point of
having this rather than writing the insert by hand. An engine with a
last insert id reports one; an engine without gets a RETURNING clause
added. An engine that can do neither raises rather than returning
something that is not an id.
var id = db.insert('posts', { title: 'Hello', body: 'World' })
Parameters
table(string)values(dict) — Column to value.options(dict|nil) —{ returning }names the column to read the id from, which matters on an engine usingRETURNINGwhen the key is not calledid.
Returns any — The new row’s id, or nil where the engine reports
none and no returning column was named.
Connection.insert_many()
sql.Connection.insert_many(table: string, rows: list) -> number
Inserts several rows in one statement.
Every row has to name the same columns. Large batches are split so that no one statement binds more parameters than the engine allows.
Parameters
table(string)rows(list) — Dictionaries of column to value.
Returns number — How many rows were inserted.
Connection.update()
sql.Connection.update(table: string, values: dict, where) -> number
Updates the rows matching where.
Passing nil for where updates every row, which has to be asked for
rather than happening because a dictionary came out empty.
Parameters
table(string)values(dict) — Column to new value.where(dict|nil) — Column to value; a list matches any of them and nil matches null.
Returns number — How many rows changed.
Connection.delete()
sql.Connection.delete(table: string, where) -> number
Deletes the rows matching where.
Parameters
table(string)where(dict|nil)
Returns number — How many rows went.
Connection.find()
sql.Connection.find(table: string, where, options) -> ResultSet
Selects the rows matching where.
db.find('posts', { published: true }, {
columns: ['id', 'title'],
order: ['created_at desc'],
limit: 10,
})
Parameters
table(string)where(dict|nil)options(dict|nil) —columns,order,limitandoffset.
Returns ResultSet
Connection.find_one()
sql.Connection.find_one(table: string, where, options) -> dict|nil
The first row matching where, or nil.
Parameters
table(string)where(dict|nil)options(dict|nil)
Returns dict|nil
Connection.count()
sql.Connection.count(table: string, where) -> number
How many rows match where.
Parameters
table(string)where(dict|nil)
Returns number
Connection.transaction()
sql.Connection.transaction(body: function, isolation) -> any
Runs body inside a transaction, committing when it returns and rolling
back when it raises.
db.transaction(@(tx) {
tx.exec('insert into audit (what) values (?)', ['transfer'])
tx.update('accounts', { balance: 0 }, { id: 1 })
})
A transaction() called inside another takes a savepoint, so only the
outermost commits and a failure inside undoes just that inner piece of
work.
Parameters
body(function) — Called with aTransaction.isolation(string|nil) — One of the level constants, for the outermost transaction only.
Returns any — Whatever body returned.
Connection.begin()
sql.Connection.begin(isolation) -> Transaction
Opens a transaction to be committed or rolled back by hand.
The closure form is safer and should be preferred. This exists for a transaction whose lifetime is not a block, such as one held open across an entire request.
Parameters
isolation(string|nil)
Returns Transaction
Connection.in_transaction()
sql.Connection.in_transaction() -> bool
Whether a transaction is open on this connection.
Returns bool
Connection.ping()
sql.Connection.ping() -> bool
Checks the connection is still usable.
Returns bool
Connection.close()
sql.Connection.close()
Closes the connection, or returns it to its pool when it came from one.
Safe to call more than once.
Connection.is_closed()
sql.Connection.is_closed() -> bool
Whether this connection has been closed.
Returns bool
Connection.to_string()
sql.Connection.to_string()
2026, Richard Ore and Zuri contributors
sql.crud
import sql.crud
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.crud.*needsimport sql.crud.
Building the four statements that are the same everywhere.
Inserting a row, updating rows that match, deleting rows that match and selecting rows that match are written the same way against every engine, apart from how identifiers are quoted and how placeholders are spelled. Both of those come from the driver, so these builders produce correct SQL for whichever adapter is active.
This is not a query builder and is not trying to become one. There is no join here, no subquery, no expression tree. Anything past a flat list of equality where is written as SQL, which is a better language for it than any chain of method calls.
Every value becomes a bound parameter. Nothing built here interpolates a value into the statement text, which is what makes these safe to hand user input.
Functions
raw()
sql.raw(sql: string) -> Raw
Builds a Raw.
Parameters
sql(string)
Returns Raw
quote_table()
sql.crud.quote_table(driver, table: string) -> string
Renders a table name, which may carry a schema.
'public.users' becomes "public"."users", and a name with a dot that
is genuinely part of it can be quoted by passing it already quoted.
Parameters
driver(Driver)table(string)
Returns string
where()
sql.crud.where(driver, filter, start: number) -> dict
Builds the WHERE clause for a dictionary of conditions.
A value of nil becomes IS NULL rather than = NULL, which no row
ever satisfies. A list becomes IN (...), and an empty list becomes a
condition no row satisfies, which is what “in nothing” means. A Raw is
used as written.
Parameters
driver(Driver)filter(dict|nil)start(number) — The binding position the first parameter takes.
Returns dict — { clause, values }, the clause empty when there
are no conditions.
insert()
sql.crud.insert(driver, table: string, values: dict, returning) -> dict
Builds an INSERT.
Parameters
driver(Driver)table(string)values(dict)returning(string|nil) — A column to return, for engines that report an inserted id that way rather than through a last insert id. Ignored where the driver does not supportRETURNING.
Returns dict — { sql, values }
Raises QueryError if values is empty.
insert_many()
sql.crud.insert_many(driver, table: string, rows: list) -> dict
Builds an INSERT carrying several rows in one statement.
Every row has to name the same columns, in any order; a row that names a different set raises rather than being padded with nulls, because a missing column and a null column mean different things.
Parameters
driver(Driver)table(string)rows(list) — Dictionaries of column to value.
Returns dict — { sql, values }
Raises QueryError if rows is empty, a row has no columns, or the
rows disagree about which columns they have.
update()
sql.crud.update(driver, table: string, values: dict, filter) -> dict
Builds an UPDATE.
An update with no where changes every row, which is occasionally what
someone means and usually not, so it has to be asked for by passing
nil rather than happening by default when a condition dictionary turns
out empty.
Parameters
driver(Driver)table(string)values(dict)filter(dict|nil)
Returns dict — { sql, values }
Raises QueryError if values is empty.
delete()
sql.crud.delete(driver, table: string, filter) -> dict
Builds a DELETE.
Parameters
driver(Driver)table(string)filter(dict|nil)
Returns dict — { sql, values }
select()
sql.crud.select(driver, table: string, filter, options) -> dict
Builds a SELECT.
Parameters
driver(Driver)table(string)filter(dict|nil)options(dict|nil) —columns(a list of names, all of them by default),order(a list of names, each optionally followed bydesc),limitandoffset.
Returns dict — { sql, values }
Classes
Raw
class sql.Raw
Marks a fragment of SQL to be used as written rather than bound.
The escape hatch for the cases where a value is not a value:
db.update('posts', { views: sql.raw('views + 1') }, { id: 7 })
It is exactly as dangerous as it sounds. A Raw built from anything a
user supplied is a SQL injection, so build them from literals and keep
values in the parameters where they belong.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
sql | string | The fragment, as it will appear in the statement. |
Constructor
sql.Raw(sql)
Parameters
sql(string)
Raw.to_string()
sql.Raw.to_string()
2026, Richard Ore and Zuri contributors
sql.cursor
import sql.cursor
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.cursor.*needsimport sql.cursor.
Reading a result without holding all of it.
query() builds the whole result in memory, which is the right thing
for the hundreds of rows most queries return and the wrong thing for the
millions some do. A cursor reads the same result a row at a time, so the
memory a loop needs is the size of one row rather than of the table.
var cursor = db.stream('select * from events')
for row in cursor {
handle(row)
}
Running to the end closes the cursor. A loop that stops early does not,
so anything that might break should close it, which using makes hard
to forget.
Classes
Cursor
class sql.Cursor
A result being read a row at a time.
- printable — has a
@to_string(), soechoandprint()show something useful - iterable — can be walked with
foranditer
Constructor
sql.Cursor(cursor)
Parameters
cursor(DriverCursor) — The adapter’s own cursor.
Cursor.columns()
sql.Cursor.columns() -> list[dict]
The result’s columns, each a dictionary with name and type.
Returns list[dict]
Cursor.column_names()
sql.Cursor.column_names() -> list[string]
The names of the result’s columns, in order.
Returns list[string]
Cursor.position()
sql.Cursor.position() -> number
How many rows have been read so far.
Returns number
Cursor.next()
sql.Cursor.next() -> dict|nil
The next row as a dictionary keyed by column name, or nil once the result is finished.
Returns dict|nil
Raises ClosedError if the cursor was closed.
Cursor.take()
sql.Cursor.take(count: number) -> list[dict]
The next count rows, or fewer at the end of the result.
For work that batches naturally: inserting a thousand rows at a time into somewhere else, say.
Parameters
count(number)
Returns list[dict]
Cursor.rest()
sql.Cursor.rest() -> list[dict]
Reads whatever is left into a list.
This gives up the point of a cursor, so it is for the case where a result turned out to be small after all.
Returns list[dict]
Cursor.is_closed()
sql.Cursor.is_closed() -> bool
Whether the cursor has been closed or read to the end.
Returns bool
Cursor.close()
sql.Cursor.close()
Releases the cursor. Safe to call more than once.
Cursor.to_string()
sql.Cursor.to_string()
2026, Richard Ore and Zuri contributors
sql.decimal
import sql.decimal
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.decimal.*needsimport sql.decimal.
Exact decimal numbers, for the money column that must not be a float.
PostgreSQL’s numeric holds a decimal value exactly. A number in Zuri
is a double, which holds 0.1 only approximately, so reading a balance
of 0.10 into one and writing it back is not guaranteed to store what
was there before. Decimal keeps the value as an integer and a scale,
so 10.05 is exactly ten and five hundredths and stays that way through
every read and write.
var price = Decimal('19.99')
var tax = price.multiply(Decimal('0.20'))
echo tax.to_string()
# 3.9980
Addition, subtraction and multiplication are exact. Division is not, in general, so it takes the number of decimal places to produce and rounds half away from zero, the rule money is normally counted by.
Functions
decimal()
sql.decimal(value, scale) -> Decimal
Builds a decimal, the same as Decimal() but as a function.
Parameters
value(string|number|bigint)scale(number|nil)
Returns Decimal
Classes
Decimal
class sql.Decimal
An exact decimal number.
Held as an integer and a scale: unscaled is the digits with the point
removed, scale is how many of them are after the point. So 1.05 is
105 at scale 2, and 1.050 is 1050 at scale 3. The two are equal in
value and differ in how they print, which is what lets a column declared
numeric(10, 3) round trip without gaining or losing a trailing zero.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
unscaled | bigint | The digits, with the decimal point removed, as a bigint. |
scale | number | How many digits are after the decimal point. |
Constructor
sql.Decimal(value, scale)
Builds a decimal.
From text, which is the exact form and the one to prefer:
Decimal('19.99')
Decimal('-0.0001')
Decimal('1.2e3')
From a number, which is only as exact as the double was:
Decimal(19.99)
Or from the parts, which is what the PostgreSQL adapter uses:
Decimal(1999, 2)
Parameters
value(string|number|bigint) — The value, or its unscaled digits whenscaleis given.scale(number|nil) — The number of decimal places, whenvalueis the unscaled integer.
Raises QueryError if the text is not a number.
Decimal.is_negative()
sql.Decimal.is_negative() -> bool
Whether this is negative.
Returns bool
Decimal.is_zero()
sql.Decimal.is_zero() -> bool
Whether this is exactly zero, whatever its scale.
Returns bool
Decimal.rescale()
sql.Decimal.rescale(places: number) -> Decimal
The same value at a different number of decimal places.
Adding places is exact. Removing them rounds half away from zero, so
2.5 to no places is 3 and -2.5 is -3.
Parameters
places(number)
Returns Decimal
Decimal.add()
sql.Decimal.add(other) -> Decimal
The sum of this and other, exactly.
The result carries the larger of the two scales, so no digit is lost.
Parameters
other(Decimal)
Returns Decimal
Decimal.subtract()
sql.Decimal.subtract(other) -> Decimal
The difference, exactly.
Parameters
other(Decimal)
Returns Decimal
Decimal.multiply()
sql.Decimal.multiply(other) -> Decimal
The product, exactly.
The scales add, as they do when multiplying by hand: two places times two places is four places.
Parameters
other(Decimal)
Returns Decimal
Decimal.divide()
sql.Decimal.divide(other, places: number) -> Decimal
The quotient to places decimal places, rounded half away from zero.
Division is the one operation with no exact answer in general, so the number of places is required rather than guessed.
Parameters
other(Decimal)places(number)
Returns Decimal
Raises QueryError if other is zero.
Decimal.to_number()
sql.Decimal.to_number() -> number
The value as a number, which is approximate for anything a double
cannot hold exactly.
Returns number
Decimal.compare()
sql.Decimal.compare(other) -> number
Compares this with other: negative, zero or positive, the same shape a
sort comparator takes.
Parameters
other(Decimal)
Returns number
Decimal.equals()
sql.Decimal.equals(other) -> bool
Whether this and other are the same value, whatever their scales.
1.5 and 1.50 are equal.
Parameters
other(Decimal)
Returns bool
Decimal.to_string()
sql.Decimal.to_string() -> string
The value written out, with exactly scale digits after the point.
Returns string
2026, Richard Ore and Zuri contributors
sql.driver
import sql.driver
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.driver.*needsimport sql.driver.
The contract every database adapter implements, and the capability flags that let one adapter differ from another without the code above them having to know which is in use.
An adapter supplies four things: a Driver that knows how to read a
connection string and open a connection, a DriverConnection that runs
statements, a DriverStatement for a prepared one, and a DriverCursor
for reading a result a batch at a time. Everything a program touches
directly is built on top of these, so an adapter that implements them
gets pooling, transactions, the CRUD helpers, placeholder translation
and the error hierarchy without writing any of it.
The methods here raise NotImplementedError. An adapter that forgets
one fails where the gap is rather than somewhere further down, and an
adapter for an engine that genuinely cannot do something raises
NotSupportedError from its own override instead, which reads
differently on purpose: the first is an unfinished adapter, the second
is an honest limit of the engine.
Constants
READ_COMMITTED
sql.READ_COMMITTED = 'read committed'
Each transaction sees rows committed before its own statement started, and nothing a concurrent transaction commits afterwards within that statement.
REPEATABLE_READ
sql.REPEATABLE_READ = 'repeatable read'
A transaction reads the same rows throughout, whatever anyone else commits while it runs.
SERIALIZABLE
sql.SERIALIZABLE = 'serializable'
Concurrent transactions produce a result some serial order of them could
also have produced. The engine may abort one to keep that promise, which
surfaces as SerializationError.
READ_UNCOMMITTED
sql.READ_UNCOMMITTED = 'read uncommitted'
A transaction can see rows another has written and not committed. PostgreSQL accepts the name and gives read committed anyway, which its own documentation is explicit about.
Functions
default_capabilities()
sql.default_capabilities() -> dict
The capability flags a driver reports, with the value each takes when a driver does not say otherwise.
A driver’s capabilities() starts from this and overrides what differs,
so a flag added here later does not break an adapter written before it
existed.
| Flag | Meaning |
|---|---|
placeholder_style | How the engine spells a parameter. |
named_parameters | Whether the engine binds parameters by name. |
last_insert_id | Whether the engine reports the id of the row just inserted. |
returning | Whether INSERT ... RETURNING works. |
transactions | Whether BEGIN, COMMIT and ROLLBACK work. |
savepoints | Whether SAVEPOINT works, which is what nested transactions are built on. |
transactional_ddl | Whether CREATE, ALTER and DROP stay inside |
| a transaction, rather than committing it. | |
isolation_levels | The levels the engine will accept. |
server_side_cursors | Whether a result can be read without the whole of it arriving first. |
prepared_statements | Whether a statement can be compiled once and run repeatedly. |
multiple_statements | Whether one call may carry several |
| statements separated by semicolons. | |
arrays | Whether the engine has an array type. |
json | Whether the engine has a JSON type, as opposed to storing JSON in text. |
blobs | Whether the engine stores binary values. |
decimals | Whether the engine has an exact decimal type. |
booleans | Whether the engine has a real boolean type. |
upsert | Whether an insert can update the row it collided |
| with, however the engine spells it. | |
schemas | Whether tables live in named schemas. |
concurrent_writers | Whether two connections can write at once. |
max_parameters | Most parameters one statement may bind, or nil for no practical limit. |
identifier_quote | The character an identifier is quoted with. |
default_port | The port used when a connection string omits |
| one, or nil for an engine with no port. |
Returns dict
Classes
Driver
class sql.Driver
Opens connections to one kind of database.
A driver holds no connection of its own and no state worth sharing, so
one instance per engine is registered with sql.register() and reused
for every connection it opens.
Driver.name()
sql.Driver.name() -> string
The name this driver is registered under, such as 'sqlite'.
Appears on every error the adapter raises, so it wants to be the name a reader would recognise.
Returns string
Driver.schemes()
sql.Driver.schemes() -> list[string]
The URL schemes this driver claims, without the ://.
sql.open() picks a driver by matching a connection string’s scheme
against these, which is what lets the same call open any of the engines.
Several are allowed: PostgreSQL answers to both postgres and
postgresql.
Returns list[string]
Driver.capabilities()
sql.Driver.capabilities() -> dict
What this engine can and cannot do.
Built from default_capabilities() with the differences applied.
Returns dict
Driver.parse_dsn()
sql.Driver.parse_dsn(dsn) -> dict
Turns a connection string or an options dictionary into the normalised
options connect() takes.
Each adapter reads the forms its own engine’s tooling uses, so a connection string copied from somewhere else works unchanged.
Parameters
dsn(string|dict)
Returns dict
Driver.connect()
sql.Driver.connect(options: dict) -> DriverConnection
Opens a connection with options from parse_dsn().
Parameters
options(dict)
Returns DriverConnection
Driver.quote_identifier()
sql.Driver.quote_identifier(name: string) -> string
Quotes name so the engine reads it as an identifier whatever it
contains or collides with.
The default doubles the engine’s own quote character, which is what the SQL standard says and what the built-in adapters do, MySQL included once its quote character is taken into account. An engine whose quoting differs overrides this.
Parameters
name(string)
Returns string
Driver.supports()
sql.Driver.supports(flag: string) -> bool
Whether this driver supports flag.
Parameters
flag(string) — A key ofdefault_capabilities().
Returns bool
DriverConnection
class sql.DriverConnection
One open connection to a database, as the layer above it sees one.
Adapters implement this; programs use sql.Connection, which wraps it
and adds everything that is the same across engines.
Fields
| Field | Type | Description |
|---|---|---|
driver | Driver | The Driver that opened this connection. |
DriverConnection.execute()
sql.DriverConnection.execute(sql: string, values: list) -> dict
Runs a statement for its effect and reports what it did.
Parameters
sql(string) — Already translated into this engine’s own placeholder style.values(list) — Bound in order.
Returns dict — { rows_affected, last_insert_id }, the second nil
on an engine that does not report one.
DriverConnection.select()
sql.DriverConnection.select(sql: string, values: list) -> dict
Runs a statement and reads its whole result.
Parameters
sql(string)values(list)
Returns dict — { columns, rows }, where columns is a list of
{ name, type } dictionaries and rows a list of value lists in the
same order.
DriverConnection.open_cursor()
sql.DriverConnection.open_cursor(sql: string, values: list, options: dict) -> DriverCursor
Runs a statement and returns a cursor over its result rather than reading all of it.
On an engine without server side cursors this still avoids holding every row at once only as far as the engine allows; the capability flag says which.
Parameters
sql(string)values(list)options(dict) —{ batch }, the rows to fetch at a time.
Returns DriverCursor
DriverConnection.prepare()
sql.DriverConnection.prepare(sql: string) -> DriverStatement
Compiles a statement for repeated use.
Parameters
sql(string)
Returns DriverStatement
DriverConnection.begin()
sql.DriverConnection.begin(isolation)
Opens a transaction.
Parameters
isolation(string|nil) — One of the level constants, or nil for the engine’s default.
DriverConnection.commit()
sql.DriverConnection.commit()
Commits the open transaction.
DriverConnection.rollback()
sql.DriverConnection.rollback()
Rolls the open transaction back.
DriverConnection.savepoint()
sql.DriverConnection.savepoint(name: string)
Marks a point inside the open transaction that can be returned to.
Parameters
name(string)
DriverConnection.release_savepoint()
sql.DriverConnection.release_savepoint(name: string)
Discards a savepoint, keeping everything done since it was taken.
Parameters
name(string)
DriverConnection.rollback_to()
sql.DriverConnection.rollback_to(name: string)
Undoes everything done since name was taken, leaving the transaction
open and the savepoint still in place.
Parameters
name(string)
DriverConnection.in_transaction()
sql.DriverConnection.in_transaction() -> bool
Whether a transaction is currently open on this connection.
Returns bool
DriverConnection.last_insert_id()
sql.DriverConnection.last_insert_id() -> number|bigint|nil
The id of the row most recently inserted on this connection.
Only meaningful where last_insert_id is supported; elsewhere it raises
NotSupportedError, and Connection.insert() uses RETURNING instead.
Returns number|bigint|nil
DriverConnection.ping()
sql.DriverConnection.ping() -> bool
Checks the connection is still usable, cheaply.
A pool calls this before handing a connection out, so it wants to be the smallest round trip the engine offers.
Returns bool
DriverConnection.server_version()
sql.DriverConnection.server_version() -> string
The engine’s version, as the engine reports it.
Returns string
DriverConnection.schema_adapter()
sql.DriverConnection.schema_adapter() -> SchemaAdapter
This engine’s introspection, which Connection.schema goes through.
The default refuses, so an adapter that has not written the queries says so plainly rather than reporting an empty database.
Returns SchemaAdapter
Raises NotSupportedError if the adapter has no introspection.
DriverConnection.close()
sql.DriverConnection.close()
Closes the connection. Calling this more than once does nothing.
DriverConnection.is_closed()
sql.DriverConnection.is_closed() -> bool
Whether this connection has been closed.
Returns bool
DriverStatement
class sql.DriverStatement
A statement compiled once and run many times.
DriverStatement.execute()
sql.DriverStatement.execute(values: list) -> dict
Runs the statement for its effect.
Parameters
values(list)
Returns dict — { rows_affected, last_insert_id }
DriverStatement.select()
sql.DriverStatement.select(values: list) -> dict
Runs the statement and reads its whole result.
Parameters
values(list)
Returns dict — { columns, rows }
DriverStatement.open_cursor()
sql.DriverStatement.open_cursor(values: list, options: dict) -> DriverCursor
Runs the statement and returns a cursor over its result.
Parameters
values(list)options(dict)
Returns DriverCursor
DriverStatement.columns()
sql.DriverStatement.columns() -> list[dict]
The columns this statement returns, as { name, type } dictionaries.
Returns list[dict]
DriverStatement.parameter_count()
sql.DriverStatement.parameter_count() -> number
How many parameters this statement binds.
Returns number
DriverStatement.close()
sql.DriverStatement.close()
Releases the statement. Calling this more than once does nothing.
DriverCursor
class sql.DriverCursor
A result being read a batch at a time rather than all at once.
DriverCursor.columns()
sql.DriverCursor.columns() -> list[dict]
The columns of this result, as { name, type } dictionaries.
Returns list[dict]
DriverCursor.next_row()
sql.DriverCursor.next_row() -> list|nil
The next row as a list of values, or nil once the result is exhausted.
Returns list|nil
DriverCursor.close()
sql.DriverCursor.close()
Releases the cursor. Calling this more than once does nothing.
SchemaAdapter
class sql.SchemaAdapter
The introspection queries for one engine.
Every method returns the same shape whichever engine answered, which is
the whole point of routing introspection through here rather than
letting each program write its own information_schema query.
SchemaAdapter.tables()
sql.SchemaAdapter.tables(schema) -> list[string]
The names of the tables the application created, leaving out the ones the engine keeps for itself.
Parameters
schema(string|nil)
Returns list[string]
SchemaAdapter.views()
sql.SchemaAdapter.views(schema) -> list[string]
The names of the views.
Parameters
schema(string|nil)
Returns list[string]
SchemaAdapter.columns()
sql.SchemaAdapter.columns(table, schema) -> list[dict]
The columns of table, in order, each shaped as schema.column_shape()
describes.
Parameters
table(string)schema(string|nil)
Returns list[dict]
SchemaAdapter.primary_key_columns()
sql.SchemaAdapter.primary_key_columns(table, schema) -> list[string]
The columns making up table’s primary key, in key order, or an empty
list for a table with none.
Parameters
table(string)schema(string|nil)
Returns list[string]
SchemaAdapter.indexes()
sql.SchemaAdapter.indexes(table, schema) -> list[dict]
The indexes on table, each { name, columns, unique }.
Parameters
table(string)schema(string|nil)
Returns list[dict]
SchemaAdapter.foreign_keys()
sql.SchemaAdapter.foreign_keys(table, schema) -> list[dict]
The foreign keys on table, each { columns, references_table, references_columns, on_delete, on_update }.
Parameters
table(string)schema(string|nil)
Returns list[dict]
2026, Richard Ore and Zuri contributors
sql.errors
import sql
Everything here is re-exported by
sql, soimport sqlis enough and the names are called assql.*. Importingsql.errorson its own works too and reaches the same definitions.
Every error a database raises, under one root, in one shape, whatever engine it came from.
This is the part of sql that pays for itself soonest. PostgreSQL
reports a unique constraint violation as SQLSTATE 23505; SQLite
reports it as extended result code 2067. Neither number means anything
to the other, and code that matched on either would stop working the
moment the adapter changed. Every adapter translates its engine’s own
vocabulary into the classes below, so a program catches
UniqueViolation and keeps catching it after a migration.
The original is never thrown away. code and sqlstate carry whatever
the engine said, so anything genuinely engine specific is still
reachable.
Classes
SqlError
class sql.SqlError < Error
Base class for every error this module and its adapters raise.
Catch this to catch anything a database can do. The subclasses below
separate the cases worth handling differently, and every one of them is
a SqlError, so a broad catch never misses one.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
code | The engine’s own error code, as the engine spells it. | |
sqlstate | The five character SQLSTATE, for engines that report one. | |
driver | Name of the adapter that raised this, such as 'sqlite', 'postgres' or 'mysql'. | |
query | The statement that failed, as it was sent to the engine. |
Constructor
sql.SqlError(message, details)
Builds an error, optionally carrying the engine’s own detail.
details is a dictionary; any of code, sqlstate, driver and
query it holds are copied onto the error and the rest ignored.
Adapters fill it in. Application code raising one of these by hand can
leave it out entirely.
Parameters
message(string)details(dict|nil)
SqlError.to_string()
sql.SqlError.to_string()
ConnectionError
class sql.ConnectionError < SqlError
The database could not be reached, or the connection dropped.
A network failure, a refused connection, a server that is not running, a database file that cannot be opened. Retrying is often reasonable; the same query against the same data may well succeed once the cause clears.
- printable — has a
@to_string(), soechoandprint()show something useful
AuthenticationError
class sql.AuthenticationError < ConnectionError
The server rejected the credentials.
A ConnectionError, because the connection is what failed, but worth
separating: no amount of retrying fixes a wrong password.
- printable — has a
@to_string(), soechoandprint()show something useful
ProtocolError
class sql.ProtocolError < ConnectionError
The server sent something the adapter could not make sense of.
A ConnectionError because that is what it means in practice: the
conversation has lost its place, so nothing further on this connection
can be trusted and a pool holding it should discard it rather than lend
it out again.
Seeing one means either a bug in the adapter or something between it and the server that is not the server.
- printable — has a
@to_string(), soechoandprint()show something useful
TimeoutError
class sql.TimeoutError < ConnectionError
An operation ran out of time.
Raised for a connection that took too long to open, a statement that exceeded its deadline, and for SQLite’s busy timeout expiring while another writer held the database.
- printable — has a
@to_string(), soechoandprint()show something useful
QueryError
class sql.QueryError < SqlError
The engine refused the statement.
Bad syntax, an unknown table or column, a type the engine would not accept, a wrong number of parameters. These are programming errors: the same statement will fail the same way every time.
- printable — has a
@to_string(), soechoandprint()show something useful
IntegrityError
class sql.IntegrityError < SqlError
A constraint rejected the data.
Catch this to handle any constraint violation without caring which; the subclasses are there when the difference matters, as it does when a duplicate key means “already exists” and a foreign key violation means “referenced row is gone”.
- printable — has a
@to_string(), soechoandprint()show something useful
UniqueViolation
class sql.UniqueViolation < IntegrityError
A unique index or primary key already holds this value.
- printable — has a
@to_string(), soechoandprint()show something useful
ForeignKeyViolation
class sql.ForeignKeyViolation < IntegrityError
A foreign key has no matching row, or a referenced row is still in use by another table.
- printable — has a
@to_string(), soechoandprint()show something useful
NotNullViolation
class sql.NotNullViolation < IntegrityError
A column declared NOT NULL was given nothing.
- printable — has a
@to_string(), soechoandprint()show something useful
CheckViolation
class sql.CheckViolation < IntegrityError
A CHECK constraint rejected the row.
- printable — has a
@to_string(), soechoandprint()show something useful
TransactionError
class sql.TransactionError < SqlError
A transaction could not be carried out as asked.
Raised for a commit that failed, a rollback with nothing to roll back, and for nesting mistakes such as releasing a savepoint that was never taken.
- printable — has a
@to_string(), soechoandprint()show something useful
SerializationError
class sql.SerializationError < TransactionError
Two transactions could not both be treated as though they ran alone.
The engine gave up on one of them rather than produce a result that serial execution could not have produced. Retrying the whole transaction is the correct response, and usually succeeds.
- printable — has a
@to_string(), soechoandprint()show something useful
DeadlockError
class sql.DeadlockError < TransactionError
Two transactions were each waiting on the other, and the engine broke the tie by aborting this one.
As with SerializationError, the response is to retry the transaction
from the beginning.
- printable — has a
@to_string(), soechoandprint()show something useful
PoolError
class sql.PoolError < SqlError
Something went wrong with a connection pool rather than with a database.
- printable — has a
@to_string(), soechoandprint()show something useful
PoolExhaustedError
class sql.PoolExhaustedError < PoolError
Every connection was busy and none came free within the acquire timeout.
Either the pool is too small for the load or connections are being held longer than they should be; a connection acquired and not released is the usual cause.
- printable — has a
@to_string(), soechoandprint()show something useful
NotSupportedError
class sql.NotSupportedError < SqlError
The engine cannot do what was asked, and no approximation would be honest.
Raised where an adapter would otherwise have to guess: asking PostgreSQL for a last insert id it does not report, asking SQLite for an isolation level it has no analogue for, connecting to PostgreSQL over a Unix domain socket. The message names both the adapter and the feature.
- printable — has a
@to_string(), soechoandprint()show something useful
ClosedError
class sql.ClosedError < SqlError
The connection, statement, cursor, transaction or pool was already closed.
Closing something twice is always allowed and never raises this. Using something after closing it is what does.
- printable — has a
@to_string(), soechoandprint()show something useful
2026, Richard Ore and Zuri contributors
sql.mysql
import sql.mysql
sqldoes not re-export this module, so it is reached only by importing it directly.
The MySQL and MariaDB adapter.
Reach it through sql, which is what keeps a program portable:
import sql
var db = sql.open('mysql://alice:secret@localhost/app')
The module underneath is here for the parts of MySQL that have no equivalent elsewhere: the server’s own settings, the connection’s id, and which of the two servers answered.
What this adapter speaks
The client/server protocol over TCP, optionally under TLS and optionally
compressed with zlib or zstd, or over a unix domain socket. A socket
option names one, and so does a host beginning with a slash. TLS is
neither offered nor wanted there, since nothing sits in between, and the
connection counts as private, which is what decides whether a plugin may
send the password itself.
A statement with no values is sent as text, which is one round trip. A statement with values is prepared, so that the values travel in the server’s binary encoding rather than being written into the SQL. Prepared statements are kept and reused, so a query run in a loop is compiled once.
Authentication
mysql_native_password, caching_sha2_password (both the cached
exchange and the slower one behind it), sha256_password,
mysql_clear_password and MariaDB’s client_ed25519. On a connection
that is not private, the two plugins that need the password itself
encrypt it to the server’s public key rather than sending it, and
mysql_clear_password is refused outright.
Submodules
| Module | Reached as | Summary |
|---|---|---|
sql.mysql.auth | sql.mysql.auth.* | The authentication plugins a MySQL or MariaDB server can ask for. |
sql.mysql.connection | sql.mysql.connection.* | One open MySQL connection, as the layer above it sees one. |
sql.mysql.cursor | sql.mysql.cursor.* | Reading a MySQL result a batch at a time. |
sql.mysql.driver | sql.mysql.driver.* | Opening MySQL and MariaDB connections: reading a connection string, establishing the socket, upgrading it to… |
sql.mysql.ed25519 | import sql.mysql.ed25519 | Ed25519 signing over a key derived from a password, which is what MariaDB’s client_ed25519 authentication… |
sql.mysql.errors | sql.mysql.errors.* | Turning MySQL’s error numbers into the shared error classes. |
sql.mysql.packets | sql.mysql.packets.* | The MySQL packet layer: framing, sequence numbering, compression and the integer and string encodings every… |
sql.mysql.protocol | sql.mysql.protocol.* | The MySQL client/server protocol: the handshake that opens a connection and the command exchange that runs… |
sql.mysql.results | import sql.mysql.results | Turning what the server answered into the shapes the shared contract asks for. |
sql.mysql.schema | import sql.mysql.schema | Introspection for MySQL, through information_schema. |
sql.mysql.statement | sql.mysql.statement.* | A statement compiled once on the server and run many times. |
sql.mysql.types | sql.mysql.types.* | What MySQL’s column types mean in Zuri, in both directions and in both of the protocol’s two encodings. |
Constants
DRIVER
sql.mysql.DRIVER
The driver instance sql registers for MySQL.
MARIADB_DRIVER
sql.mysql.MARIADB_DRIVER
The driver instance sql registers for MariaDB.
Functions
connect()
sql.mysql.connect(dsn) -> MySqlConnection
Opens a MySQL connection directly, without going through sql.
Anything meant to stay portable should use sql.open() instead.
Parameters
dsn(string|dict) — A connection string or options.
Returns MySqlConnection
2026, Richard Ore and Zuri contributors
sql.mysql.auth
import sql.mysql.auth
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.mysql.auth.*needsimport sql.mysql.auth.
The authentication plugins a MySQL or MariaDB server can ask for.
Every one of them works from the same starting point: the server sends a random scramble in its greeting, and the client answers with something that proves it knows the password without containing it. What differs is the proof.
| Plugin | Proof |
|---|---|
mysql_native_password | SHA-1 of the password, masked with the scramble. |
caching_sha2_password | The same in SHA-256, with a fallback for an uncached password. |
sha256_password | The password itself, under TLS or RSA. |
client_ed25519 | A signature over the scramble, by a key derived from the password. |
mysql_clear_password | The password itself. Refused unless the connection is private. |
The two that send the password
sha256_password and the slow half of caching_sha2_password send the
password rather than a proof, because the server needs it to compute the
hash it will cache. That is safe over TLS and over a unix socket, and
over neither otherwise, so on a plain TCP connection the password is
encrypted to a public key the server hands over first.
mysql_clear_password has no such fallback and is refused on a
connection that is not already private.
Constants
NATIVE
sql.mysql.NATIVE = 'mysql_native_password'
MySQL’s original plugin, and still MariaDB’s default.
CACHING_SHA2
sql.mysql.CACHING_SHA2 = 'caching_sha2_password'
The default since MySQL 8.0.
SHA256
sql.mysql.SHA256 = 'sha256_password'
MySQL 5.7’s stronger option, superseded by the caching one.
CLEAR
sql.mysql.CLEAR = 'mysql_clear_password'
Sends the password as it is. Needs a private connection.
ED25519
sql.mysql.ED25519 = 'client_ed25519'
MariaDB’s signature-based plugin.
NONE
sql.mysql.auth.NONE = 'mysql_old_password'
Named in a handshake by a server that wants no password at all.
SUPPORTED
sql.mysql.auth.SUPPORTED = [...]
The plugins this adapter can answer.
Functions
supports()
sql.mysql.auth.supports(plugin: string) -> bool
Whether plugin is one this adapter knows how to answer.
Parameters
plugin(string)
Returns bool
native_password()
sql.mysql.auth.native_password(password: string, scramble) -> bytes
The mysql_native_password response.
The server stores SHA-1 of SHA-1 of the password. Hashing the scramble together with what the server stores produces a mask that both sides can compute, and the client returns its own first stage hash under that mask. The server unmasks it, hashes it once more, and compares. The password never crosses the wire and neither does anything that would work a second time.
Parameters
password(string)scramble(bytes) — The 20 bytes from the greeting.
Returns bytes — The 20 byte response, or nothing for an empty
password, which is what the server expects for an account with none.
caching_sha2_password()
sql.mysql.auth.caching_sha2_password(password: string, scramble) -> bytes
The fast caching_sha2_password response.
The same construction in SHA-256, with the scramble on the other side of the hash. It only succeeds where the server already holds this account’s hash in memory; a server that does not answers with a request for the password itself.
Parameters
password(string)scramble(bytes)
Returns bytes — The 32 byte response, or nothing for an empty
password.
ed25519_password()
sql.mysql.auth.ed25519_password(password: string, scramble) -> bytes
The client_ed25519 response, which MariaDB uses.
The password is hashed into an Ed25519 key and the scramble is signed with it. The server holds only the matching public key, so unlike every hash based plugin here, what the server stores cannot be replayed against it.
Parameters
password(string)scramble(bytes)
Returns bytes — The 64 byte signature.
clear_password()
sql.mysql.auth.clear_password(password: string) -> bytes
The password as the server reads it when it is sent outright: the bytes and the zero that ends them.
Parameters
password(string)
Returns bytes
obfuscate()
sql.mysql.auth.obfuscate(password: string, scramble) -> bytes
The password masked with the scramble, which is the form the two RSA plugins encrypt.
Masking adds nothing against someone who can read the ciphertext, since they cannot. It matters because the scramble differs every connection, so the same password never encrypts to the same bytes twice, and a captured exchange cannot be replayed.
Parameters
password(string)scramble(bytes)
Returns bytes
encrypt_password()
sql.mysql.auth.encrypt_password(password: string, scramble, public_pem: string) -> bytes
The password encrypted to the server’s public key, for the plugins that need the password itself over a connection that is not private.
MySQL decrypts this with OAEP under SHA-1, which is what its own server links against. The choice is the server’s, not this adapter’s.
Parameters
password(string)scramble(bytes)public_pem(string) — The key the server sent.
Returns bytes
Raises AuthenticationError if the key will not parse.
response_for()
sql.mysql.auth.response_for(plugin: string, password: string, scramble, private: bool) -> bytes
The first response to send for plugin, before any back and forth.
Parameters
plugin(string)password(string)scramble(bytes)private(bool) — Whether the connection is already private, which decides whether a plugin may send the password outright.
Returns bytes
Raises AuthenticationError if the plugin is one this adapter
cannot answer, or would have to answer unsafely.
2026, Richard Ore and Zuri contributors
sql.mysql.connection
import sql.mysql.connection
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.mysql.connection.*needsimport sql.mysql.connection.
One open MySQL connection, as the layer above it sees one.
Why a statement with values is always prepared
MySQL has two ways to run a statement. Sent as text it is one round trip, but there is nowhere to put a value: it would have to be written into the SQL, which means quoting it, which means being wrong about quoting it eventually. Prepared, the values travel separately in the binary encoding and the question never arises.
So a statement with no values goes as text and a statement with values is prepared. Preparing costs a round trip, which is why a connection keeps the statements it has compiled and reuses them: a query run in a loop pays for it once.
Constants
CACHE_LIMIT
sql.mysql.connection.CACHE_LIMIT = 64
How many compiled statements a connection keeps before releasing the one it has gone longest without using.
Each costs a little memory on the server, so this is a ceiling rather than a target.
Classes
MySqlConnection
class sql.mysql.MySqlConnection < DriverConnection
A connection to MySQL or MariaDB.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.mysql.MySqlConnection(driver, protocol, options)
Parameters
driver(Driver)protocol(Protocol) — An authenticated protocol.options(dict)
MySqlConnection.protocol()
sql.mysql.MySqlConnection.protocol() -> Protocol
The protocol underneath, for the MySQL-only features that are not part of the shared contract.
Returns Protocol
MySqlConnection.parameters()
sql.mysql.MySqlConnection.parameters() -> dict
What the server said about itself when it answered.
Returns dict
MySqlConnection.flavor()
sql.mysql.MySqlConnection.flavor() -> string
Which server this is: 'mysql' or 'mariadb'.
Returns string
MySqlConnection.connection_id()
sql.mysql.MySqlConnection.connection_id() -> number
The id the server gave this connection.
This is what KILL names, and what a connection shows as in SHOW PROCESSLIST,
so it is the handle for stopping a statement running on this connection
from another one.
Returns number
MySqlConnection.schema_adapter()
sql.mysql.MySqlConnection.schema_adapter()
MySqlConnection.execute()
sql.mysql.MySqlConnection.execute(sql: string, values: list)
MySqlConnection.select()
sql.mysql.MySqlConnection.select(sql: string, values: list)
MySqlConnection.open_cursor()
sql.mysql.MySqlConnection.open_cursor(sql: string, values: list, options: dict)
MySqlConnection.prepare()
sql.mysql.MySqlConnection.prepare(sql: string)
MySqlConnection.begin()
sql.mysql.MySqlConnection.begin(isolation)
Opens a transaction, or takes a savepoint inside the one already open.
Parameters
isolation(string|nil)
MySqlConnection.commit()
sql.mysql.MySqlConnection.commit()
Commits, or releases the innermost savepoint.
MySqlConnection.rollback()
sql.mysql.MySqlConnection.rollback()
Rolls back, or returns to the innermost savepoint and discards it.
MySqlConnection.savepoint()
sql.mysql.MySqlConnection.savepoint(name: string)
MySqlConnection.release_savepoint()
sql.mysql.MySqlConnection.release_savepoint(name: string)
MySqlConnection.rollback_to()
sql.mysql.MySqlConnection.rollback_to(name: string)
MySqlConnection.in_transaction()
sql.mysql.MySqlConnection.in_transaction()
MySqlConnection.last_insert_id()
sql.mysql.MySqlConnection.last_insert_id() -> number|bigint|nil
The id generated by the last insert on this connection.
Zero means the last statement generated none, which is what an insert into a table with no auto increment column reports, so it comes back as nil rather than as a row that does not exist.
Returns number|bigint|nil
MySqlConnection.ping()
sql.mysql.MySqlConnection.ping()
MySqlConnection.server_version()
sql.mysql.MySqlConnection.server_version()
MySqlConnection.reset()
sql.mysql.MySqlConnection.reset()
Returns the session to the state it had when it opened.
A pool calls this before handing the connection on, so that temporary tables, session variables and an unfinished transaction cannot leak from one borrower to the next.
MySqlConnection.close()
sql.mysql.MySqlConnection.close()
MySqlConnection.is_closed()
sql.mysql.MySqlConnection.is_closed()
MySqlConnection.run_simple()
sql.mysql.MySqlConnection.run_simple(sql: string) -> dict
Runs a statement as text, for the cases that have no values and so need no preparing.
Parameters
sql(string)
Returns dict
MySqlConnection.to_string()
sql.mysql.MySqlConnection.to_string()
2026, Richard Ore and Zuri contributors
sql.mysql.cursor
import sql.mysql.cursor
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.mysql.cursor.*needsimport sql.mysql.cursor.
Reading a MySQL result a batch at a time.
The server keeps the result and sends as many rows as each fetch asks for, so nothing but the current batch is ever held on this side. Only a prepared statement can be read this way, which is why a cursor over a statement written as text still prepares it first.
One at a time
A cursor holds the connection: the server is part way through answering, and no other command may be sent until it has finished. Running a statement on the same connection while a cursor is open raises rather than corrupting the exchange.
Classes
MySqlCursor
class sql.mysql.MySqlCursor < DriverCursor
A result being read in batches.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.mysql.MySqlCursor(connection, state)
Parameters
connection(MySqlConnection)state(dict) — The statement to fetch from, its columns, the batch size, whether there is anything to fetch at all, and whether this cursor prepared the statement and so has to release it.
MySqlCursor.columns()
sql.mysql.MySqlCursor.columns()
MySqlCursor.next_row()
sql.mysql.MySqlCursor.next_row() -> list|nil
The next row as a list of values, or nil once the result is finished.
Returns list|nil
MySqlCursor.close()
sql.mysql.MySqlCursor.close()
Releases the cursor, and the statement if this cursor prepared one. Safe to call more than once.
MySqlCursor.is_closed()
sql.mysql.MySqlCursor.is_closed() -> bool
Whether the cursor has been closed or read to the end.
Returns bool
MySqlCursor.to_string()
sql.mysql.MySqlCursor.to_string()
2026, Richard Ore and Zuri contributors
sql.mysql.driver
import sql.mysql.driver
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.mysql.driver.*needsimport sql.mysql.driver.
Opening MySQL and MariaDB connections: reading a connection string, establishing the socket, upgrading it to TLS when asked, and authenticating.
Constants
DEFAULT_PORT
sql.mysql.DEFAULT_PORT = 3306
The port a MySQL server listens on unless told otherwise.
DEFAULT_TIMEOUT
sql.mysql.driver.DEFAULT_TIMEOUT = 30000
How long to wait for the socket and for each read, in milliseconds.
MAX_PARAMETERS
sql.mysql.MAX_PARAMETERS = 65535
Most parameters one statement can bind. The protocol counts them in a 16 bit field, so this is a hard limit rather than a configured one.
SSL_MODES
sql.mysql.SSL_MODES = [...]
What sslmode can be.
| Mode | Meaning |
|---|---|
disable | Never use TLS. |
prefer | Where the server offers it, unverified; in the clear where it does not. |
require | Insist on TLS, without checking who the server is. |
verify | Insist on TLS and check the server’s certificate and host name. |
MySQL’s own spellings are accepted too. DISABLED, PREFERRED and
REQUIRED mean what they say; VERIFY_CA and VERIFY_IDENTITY both
become verify. That makes VERIFY_CA stricter here than on the
command line, where it checks the certificate but not the name: the
difference can only cause a connection to be refused, never to be
wrongly trusted.
Classes
MySqlDriver
class sql.mysql.MySqlDriver < Driver
Opens MySQL connections.
MySqlDriver.name()
sql.mysql.MySqlDriver.name()
MySqlDriver.schemes()
sql.mysql.MySqlDriver.schemes()
MySqlDriver.capabilities()
sql.mysql.MySqlDriver.capabilities() -> dict
What MySQL can do.
last_insert_id is true and returning is false, which together are
the most visible difference from PostgreSQL: an insert that wants its id
back reads it from what the server reported rather than from a
RETURNING clause, and Connection.insert() picks between the two on
its own.
Returns dict
MySqlDriver.parse_dsn()
sql.mysql.MySqlDriver.parse_dsn(dsn) -> dict
Reads a connection string, in either of the two forms MySQL’s own tooling accepts.
A URI:
mysql://alice:secret@db.example.com:3306/app?sslmode=verify
mysql://localhost/app
Or keywords, as my.cnf spells them:
host=localhost port=3306 database=app user=alice password=secret
Anything absent is filled in the way the command line client fills it:
the user defaults to root, the host to 127.0.0.1 and the port to
3306. No database is selected unless one is named.
Parameters
dsn(string|dict)
Returns dict
Raises ConnectionError if the string cannot be read.
MySqlDriver.connect()
sql.mysql.MySqlDriver.connect(options: dict) -> MySqlConnection
Opens the connection and authenticates.
Parameters
options(dict) — Fromparse_dsn().
Returns MySqlConnection
MariaDbDriver
class sql.mysql.MariaDbDriver < MySqlDriver
Opens MariaDB connections.
MariaDB speaks the same protocol, so everything about the exchange is
shared. It differs in what it can be asked to do: RETURNING works
there and does not on MySQL, which is the one capability that changes
what the layer above generates.
A MariaDB server reached through mysql:// works, and simply reports
the smaller set of capabilities.
MariaDbDriver.name()
sql.mysql.MariaDbDriver.name()
MariaDbDriver.schemes()
sql.mysql.MariaDbDriver.schemes()
MariaDbDriver.capabilities()
sql.mysql.MariaDbDriver.capabilities()
2026, Richard Ore and Zuri contributors
sql.mysql.ed25519
import sql.mysql.ed25519
sqldoes not re-export this module, so it is reached only by importing it directly.
Ed25519 signing over a key derived from a password, which is what
MariaDB’s client_ed25519 authentication is built on.
Why this is not crypto.ed25519
An ordinary Ed25519 private key is a 32 byte seed, and signing begins by hashing that seed into the 64 bytes the signature is actually computed from. MariaDB skips the seed: it hashes the password into those 64 bytes directly, so the user’s password takes the place of the expanded key rather than the place of the seed.
Nothing in crypto.ed25519 can express that, since it takes a
PEM-encoded key and does its own expansion internally. So the curve
arithmetic lives here, where the one thing that needs it is.
Correctness
Everything below the password derivation is Ed25519 exactly as RFC 8032
specifies it, and is checked against that document’s test vectors and
against crypto.ed25519 over random keys.
Constants
P
sql.mysql.ed25519.P
The field prime, 2^255 - 19.
L
sql.mysql.ed25519.L
The order of the base point’s subgroup.
D
sql.mysql.ed25519.D
The curve constant, -121665/121666.
D2
sql.mysql.ed25519.D2
Twice the curve constant, which is what the addition uses.
Functions
password_key()
sql.mysql.ed25519.password_key(password: string) -> bytes
Expands a password into the 64 bytes MariaDB signs with.
Parameters
password(string)
Returns bytes
public_key()
sql.mysql.ed25519.public_key(expanded) -> bytes
The public key matching an expanded key.
Parameters
expanded(bytes) — The 64 bytes frompassword_key().
Returns bytes — The 32 byte encoded public key.
sign()
sql.mysql.ed25519.sign(expanded, message) -> bytes
Signs message with an expanded key.
Parameters
expanded(bytes) — The 64 bytes frompassword_key().message(bytes)
Returns bytes — The 64 byte signature.
2026, Richard Ore and Zuri contributors
sql.mysql.errors
import sql.mysql.errors
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.mysql.errors.*needsimport sql.mysql.errors.
Turning MySQL’s error numbers into the shared error classes.
MySQL reports two things about a failure: its own error number, such as 1062 for a duplicate key, and a five character SQLSTATE. The number is the specific one and the SQLSTATE is the portable one, so mapping works on the number where a case is worth telling apart and falls back to the SQLSTATE class otherwise. An error number this table has never seen still lands somewhere sensible.
MariaDB shares both schemes and most of the numbers, and the few it adds sit in the same SQLSTATE classes, so nothing here needs to know which server answered.
Constants
SPECIFIC
sql.mysql.errors.SPECIFIC = {...}
Error numbers specific enough to name a class of their own.
CLASSES
sql.mysql.errors.CLASSES = {...}
SQLSTATE classes, for numbers the table above does not name.
Functions
class_for()
sql.mysql.class_for(code, sqlstate) -> class
The class that best describes an error the server reported.
Parameters
code(number|nil) — MySQL’s own error number.sqlstate(string|nil)
Returns class
raise_for()
sql.mysql.raise_for(report: dict, query)
Raises the right class for a server error report.
Parameters
report(dict) —{ code, sqlstate, message }, asprotocol.parse_error()returns it.query(string|nil) — The statement that was running.
Raises SqlError always.
2026, Richard Ore and Zuri contributors
sql.mysql.packets
import sql.mysql.packets
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.mysql.packets.*needsimport sql.mysql.packets.
The MySQL packet layer: framing, sequence numbering, compression and the integer and string encodings every message above is built from.
A MySQL connection is a stream of packets, each a three byte little-endian length, a one byte sequence number, and that many bytes of payload. The sequence number restarts at zero with every command a client sends and increments for each packet either side writes until that command is answered, which is how both ends notice a packet that went missing or arrived twice.
Why a packet is not always a message
The length field is three bytes, so a payload of 16777215 bytes is the largest that can be described. A message that long is split, and the split is signalled by a packet of exactly that size: the reader has to keep reading until it sees a shorter one. A message whose length is an exact multiple of the maximum therefore ends with an empty packet, which exists only to say the message is over.
Constants
MAX_PAYLOAD
sql.mysql.packets.MAX_PAYLOAD = 16777215
The largest payload a single packet can carry. A payload of exactly this size is always continued in the next packet.
COMPRESS_THRESHOLD
sql.mysql.packets.COMPRESS_THRESHOLD = 50
Below this, a compressed packet is sent uncompressed instead. Small payloads grow rather than shrink under zlib, and the protocol allows either, so the choice is the sender’s.
LENENC_NULL
sql.mysql.packets.LENENC_NULL = 251
The first byte of a length-encoded integer that means the value is NULL. Only meaningful where a column value is expected.
EXACT_INTEGER_LIMIT
sql.mysql.packets.EXACT_INTEGER_LIMIT = 9007199254740992
Largest integer a Zuri number holds exactly. A wider value read off the wire becomes a bigint rather than quietly losing its low bits.
Classes
Writer
class sql.mysql.packets.Writer
Builds one packet payload.
Bytes accumulate in a list, which payload() turns into the real thing.
Every appending method returns the writer so that building a message
reads as one expression.
Writer.byte()
sql.mysql.packets.Writer.byte(value: number) -> Writer
Appends one byte.
Parameters
value(number) — Only the low eight bits are used.
Returns Writer
Writer.int2()
sql.mysql.packets.Writer.int2(value: number) -> Writer
Appends a little-endian 16 bit integer.
Parameters
value(number)
Returns Writer
Writer.int3()
sql.mysql.packets.Writer.int3(value: number) -> Writer
Appends a little-endian 24 bit integer.
Parameters
value(number)
Returns Writer
Writer.int4()
sql.mysql.packets.Writer.int4(value: number) -> Writer
Appends a little-endian 32 bit integer.
Values are written unsigned. A negative number is written as its two’s complement, which is what the protocol expects wherever a signed field appears.
Parameters
value(number)
Returns Writer
Writer.int8()
sql.mysql.packets.Writer.int8(value) -> Writer
Appends a little-endian 64 bit integer.
Parameters
value(number|bigint)
Returns Writer
Writer.lenenc_int()
sql.mysql.packets.Writer.lenenc_int(value) -> Writer
Appends a length-encoded integer, in the shortest form that holds it.
Parameters
value(number|bigint)
Returns Writer
Writer.lenenc_string()
sql.mysql.packets.Writer.lenenc_string(value) -> Writer
Appends a string with its length in front of it.
Parameters
value(string|bytes)
Returns Writer
Writer.cstring()
sql.mysql.packets.Writer.cstring(value: string) -> Writer
Appends a string and the zero byte that ends it.
Parameters
value(string)
Returns Writer
Writer.string()
sql.mysql.packets.Writer.string(value) -> Writer
Appends a string with no length and no terminator, which only works where the reader knows the length some other way.
Parameters
value(string|bytes)
Returns Writer
Writer.zeros()
sql.mysql.packets.Writer.zeros(count: number) -> Writer
Appends count zero bytes, for the filler fields the protocol reserves
and does not use.
Parameters
count(number)
Returns Writer
Writer.raw()
sql.mysql.packets.Writer.raw(values) -> Writer
Appends raw bytes.
Parameters
values(bytes|list)
Returns Writer
Writer.length()
sql.mysql.packets.Writer.length() -> number
How many bytes have accumulated.
Returns number
Writer.payload()
sql.mysql.packets.Writer.payload() -> bytes
The finished payload.
The buffer itself, not a copy, so the writer is done once this has been called.
Returns bytes
Cursor
class sql.mysql.packets.Cursor
Reads values out of one packet payload, keeping its own position.
Every read advances past what it read, so a message is decoded by naming its fields in order rather than by tracking offsets.
Constructor
sql.mysql.packets.Cursor(data)
Parameters
data(bytes) — One packet’s payload.
Cursor.remaining()
sql.mysql.packets.Cursor.remaining() -> number
How many bytes are left unread.
Returns number
Cursor.at_end()
sql.mysql.packets.Cursor.at_end() -> bool
Whether everything has been read.
Returns bool
Cursor.peek()
sql.mysql.packets.Cursor.peek() -> number|nil
The next byte without consuming it, or nil at the end.
Returns number|nil
Cursor.skip()
sql.mysql.packets.Cursor.skip(count: number) -> Cursor
Skips count bytes.
Parameters
count(number)
Returns Cursor
Cursor.byte()
sql.mysql.packets.Cursor.byte() -> number
Reads one byte.
Returns number
Cursor.int2()
sql.mysql.packets.Cursor.int2() -> number
Reads a little-endian 16 bit integer.
Returns number
Cursor.int3()
sql.mysql.packets.Cursor.int3() -> number
Reads a little-endian 24 bit integer.
Returns number
Cursor.int4()
sql.mysql.packets.Cursor.int4() -> number
Reads a little-endian 32 bit integer, unsigned.
Returns number
Cursor.int8()
sql.mysql.packets.Cursor.int8() -> number|bigint
Reads a little-endian 64 bit integer, unsigned.
Returns number|bigint — A bigint where the value will not fit in a
number exactly.
Cursor.lenenc_int()
sql.mysql.packets.Cursor.lenenc_int() -> number|bigint|nil
Reads a length-encoded integer.
Returns number|bigint|nil — nil where the value is NULL, which
only happens in a row.
Cursor.lenenc_bytes()
sql.mysql.packets.Cursor.lenenc_bytes() -> bytes|nil
Reads a length-encoded string as raw bytes.
Returns bytes|nil
Cursor.lenenc_string()
sql.mysql.packets.Cursor.lenenc_string() -> string|nil
Reads a length-encoded string as text.
Returns string|nil
Cursor.cstring()
sql.mysql.packets.Cursor.cstring() -> string
Reads up to the next zero byte, and past it.
A payload that simply ends is treated as terminated, since several messages end with a string whose terminator the server omits when nothing follows it.
Returns string
Cursor.fixed()
sql.mysql.packets.Cursor.fixed(count: number) -> bytes
Reads exactly count bytes.
Parameters
count(number)
Returns bytes
Cursor.rest()
sql.mysql.packets.Cursor.rest() -> bytes
Reads everything that is left.
Returns bytes
2026, Richard Ore and Zuri contributors
sql.mysql.protocol
import sql.mysql.protocol
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.mysql.protocol.*needsimport sql.mysql.protocol.
The MySQL client/server protocol: the handshake that opens a connection and the command exchange that runs everything after it.
A connection is strictly one command at a time. The client sends a command packet, the server answers with one of three shapes, and nothing else may be sent until that answer is complete:
| Answer | Shape |
|---|---|
| OK | The command did something and returned no rows. |
| ERR | The command failed, with a number and a SQLSTATE. |
| A result set | A column count, the columns, then the rows. |
Why the end of a result set is awkward
A row and the marker that ends the rows are told apart by their first
byte, and 0xfe means both “the rows are over” and “a length encoded
integer of eight bytes follows”. The length decides: a genuine end
marker is short, and a row that happens to start 0xfe is not. Servers
that negotiate CLIENT_DEPRECATE_EOF send an OK packet there instead,
which is what this adapter asks for, but the ambiguity has to be handled
anyway for the ones that do not.
Constants
CAPABILITIES
sql.mysql.CAPABILITIES = {...}
What the client asks for and what the server offers.
STATUS
sql.mysql.protocol.STATUS = {...}
What the server says about the session after each command.
COMMANDS
sql.mysql.COMMANDS = {...}
The commands this adapter sends.
CURSOR_READ_ONLY
sql.mysql.protocol.CURSOR_READ_ONLY = 1
Asks the server to keep the result open rather than send it all.
CURSOR_NONE
sql.mysql.protocol.CURSOR_NONE = 0
No cursor: the whole result comes back at once.
2026, Richard Ore and Zuri contributors
sql.mysql.results
import sql.mysql.results
sqldoes not re-export this module, so it is reached only by importing it directly.
Turning what the server answered into the shapes the shared contract asks for.
These live apart from the connection so the statement can use them too. A statement that reached for them through the connection, which already reaches for the statement, would be a circular import.
Functions
exec_result()
sql.mysql.results.exec_result(response: dict) -> dict
What a statement run for its effect reports.
A statement that returned rows still has to answer this, since
execute() may be called on anything. Its row count is zero, which is
true: it changed nothing.
Parameters
response(dict) — FromProtocol.read_response().
Returns dict — { rows_affected, last_insert_id }
select_result()
sql.mysql.results.select_result(response: dict) -> dict
What a statement run for its rows reports.
A statement that returned none still answers with the same shape and an empty result, so a caller never has to tell the two apart.
Parameters
response(dict) — FromProtocol.read_response().
Returns dict — { columns, rows }
2026, Richard Ore and Zuri contributors
sql.mysql.schema
import sql.mysql.schema
sqldoes not re-export this module, so it is reached only by importing it directly.
Introspection for MySQL, through information_schema.
A schema is a database
MySQL has no layer between the server and a database: what the SQL
standard calls a schema is what MySQL calls a database, and
information_schema.table_schema holds the database name. So the
schema argument every method here takes names a database, and leaving
it out means the one the connection is using.
Why nothing is grouped by the server
An index over several columns arrives as one row per column, and the
obvious group_concat would gather them server side. It also truncates
silently at group_concat_max_len, which is a kilobyte by default, so a
wide index would come back missing columns with nothing to say it had.
The rows are grouped here instead.
Classes
MySqlSchema
class sql.mysql.schema.MySqlSchema < SchemaAdapter
Answers introspection questions about a MySQL database.
Constructor
sql.mysql.schema.MySqlSchema(connection)
Parameters
connection(MySqlConnection)
MySqlSchema.tables()
sql.mysql.schema.MySqlSchema.tables(schema) -> list[string]
The tables the application created.
Parameters
schema(string|nil) — The database to look in, or nil for the one in use.
Returns list[string]
MySqlSchema.views()
sql.mysql.schema.MySqlSchema.views(schema) -> list[string]
The views.
Parameters
schema(string|nil)
Returns list[string]
MySqlSchema.columns()
sql.mysql.schema.MySqlSchema.columns(table: string, schema) -> list[dict]
The columns of table, in order.
The type is the one the table was declared with, varchar(64) rather
than varchar, since on MySQL the width is frequently the part worth
knowing.
Parameters
table(string)schema(string|nil)
Returns list[dict]
MySqlSchema.primary_key_columns()
sql.mysql.schema.MySqlSchema.primary_key_columns(table: string, schema) -> list[string]
The primary key columns, in key order.
MySQL names every primary key PRIMARY, whatever the table.
Parameters
table(string)schema(string|nil)
Returns list[string]
MySqlSchema.indexes()
sql.mysql.schema.MySqlSchema.indexes(table: string, schema) -> list[dict]
The indexes on table, each { name, columns, unique }.
The primary key is among them, under the name MySQL gives it.
Parameters
table(string)schema(string|nil)
Returns list[dict]
MySqlSchema.foreign_keys()
sql.mysql.schema.MySqlSchema.foreign_keys(table: string, schema) -> list[dict]
The foreign keys declared on table.
Parameters
table(string)schema(string|nil)
Returns list[dict]
2026, Richard Ore and Zuri contributors
sql.mysql.statement
import sql.mysql.statement
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.mysql.statement.*needsimport sql.mysql.statement.
A statement compiled once on the server and run many times.
Preparing sends the statement, and the server answers with the types it will bind and the columns it will return. Every execution after that sends only the values, in the binary encoding, which is both smaller than the text one and free of any question of quoting.
Classes
MySqlStatement
class sql.mysql.MySqlStatement < DriverStatement
A prepared statement.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.mysql.MySqlStatement(connection, prepared, sql)
Parameters
connection(MySqlConnection)prepared(dict) — What the server said when it compiled the statement.sql(string)
MySqlStatement.id()
sql.mysql.MySqlStatement.id() -> number
The statement’s id on the server.
Returns number
MySqlStatement.sql()
sql.mysql.MySqlStatement.sql() -> string
The statement as it was compiled.
Returns string
MySqlStatement.raw_columns()
sql.mysql.MySqlStatement.raw_columns() -> list[dict]
The columns as the server described them, with everything it said about each rather than the two fields the contract asks for.
Returns list[dict]
MySqlStatement.columns()
sql.mysql.MySqlStatement.columns()
MySqlStatement.parameter_count()
sql.mysql.MySqlStatement.parameter_count()
MySqlStatement.execute()
sql.mysql.MySqlStatement.execute(values: list)
MySqlStatement.select()
sql.mysql.MySqlStatement.select(values: list)
MySqlStatement.open_cursor()
sql.mysql.MySqlStatement.open_cursor(values: list, options: dict)
MySqlStatement.close()
sql.mysql.MySqlStatement.close()
Releases the statement on the server. Safe to call more than once.
MySqlStatement.is_closed()
sql.mysql.MySqlStatement.is_closed() -> bool
Whether this statement has been released.
Returns bool
MySqlStatement.to_string()
sql.mysql.MySqlStatement.to_string()
2026, Richard Ore and Zuri contributors
sql.mysql.types
import sql.mysql.types
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.mysql.types.*needsimport sql.mysql.types.
What MySQL’s column types mean in Zuri, in both directions and in both of the protocol’s two encodings.
Two encodings for the same values
A statement sent as text answers with every value as text, however it is
declared: an INT arrives as '42'. A prepared statement answers in
the binary encoding instead, where an INT is four bytes. Both reach
the same Zuri values, and the two paths are checked against each other
rather than written to agree by hand: dates and times decode by way of
the same text form whichever encoding carried them.
Where a type has no Zuri counterpart
BIT and GEOMETRY arrive as bytes, since neither has a shape Zuri
could represent without inventing one. SET arrives as the comma
separated text the server stores rather than as a list, because a list
going back in would be written as JSON and the column would quietly stop
matching.
Time zones
A DATETIME has no zone and a TIMESTAMP is converted to the
session’s. The adapter puts the session in UTC when it connects, so both
arrive as UTC and a date.Date written back converts from whatever
offset it carries. Turning that off is what the time_zone connection
option is for.
Constants
TYPES
sql.mysql.TYPES = {...}
The type byte the server puts on a column, and that a parameter carries back.
FLAGS
sql.mysql.FLAGS = {...}
What the server says about a column beyond its type.
BINARY_CHARSET
sql.mysql.types.BINARY_CHARSET = 63
The collation id that means a column holds bytes rather than text. It is
the only thing separating BLOB from TEXT on the wire, and BINARY
from CHAR.
UNSIGNED_FLAG
sql.mysql.types.UNSIGNED_FLAG = 128
The flag byte that goes with a parameter’s type to mark it unsigned.
Functions
is_binary()
sql.mysql.types.is_binary(column: dict) -> bool
Whether a column holds bytes rather than text.
Parameters
column(dict)
Returns bool
is_unsigned()
sql.mysql.types.is_unsigned(column: dict) -> bool
Whether a column’s values are unsigned.
Parameters
column(dict)
Returns bool
type_name_of()
sql.mysql.type_name_of(column: dict) -> string
The name of a column’s type, as MySQL itself would write it.
Parameters
column(dict)
Returns string
describe_columns()
sql.mysql.types.describe_columns(columns: list) -> list[dict]
Reduces the server’s column descriptors to the two fields the shared contract asks for.
Parameters
columns(list)
Returns list[dict]
decode_text()
sql.mysql.types.decode_text(raw, column: dict) -> any
Reads one value out of a text protocol row.
Parameters
raw(bytes|nil) — The value as the server wrote it, or nil for NULL.column(dict)
Returns any
decode_binary()
sql.mysql.types.decode_binary(cursor, column: dict) -> any
Reads one value out of a binary protocol row.
The NULL bitmap is the caller’s business; this is only reached for a value that is present.
Parameters
cursor(Cursor) — Positioned at the value.column(dict)
Returns any
parameter_type()
sql.mysql.types.parameter_type(value) -> dict
The type byte and flag a value is sent as.
write_parameter() has to agree with this exactly, since a statement
sends every type first and then every value.
Parameters
value(any)
Returns dict — { type, unsigned }
Raises QueryError for a value with no MySQL representation.
write_parameter()
sql.mysql.types.write_parameter(writer, value)
Appends a value in the form its type byte promises.
Parameters
writer(Writer)value(any)
2026, Richard Ore and Zuri contributors
sql.params
import sql.params
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.params.*needsimport sql.params.
Rewriting a statement’s placeholders into whatever the active adapter expects.
Engines disagree about how a parameter is spelled. PostgreSQL wants
$1, SQLite accepts ?1, others want a bare ?. Without a translation
step, switching adapters would mean rewriting every statement in a
program, which is most of what makes changing database an ordeal.
So sql defines one spelling and translates. Statements are written
with ? for positional parameters or :name for named ones, and each
adapter’s driver declares which form it needs.
translate('select * from t where a = ? and b = ?', [1, 2], NUMBERED)
# { sql: 'select * from t where a = $1 and b = $2', values: [1, 2] }
The scanner understands enough SQL to know when a ? or a : is not a
placeholder: inside a string, a quoted identifier, a bracketed
identifier, a comment, or a PostgreSQL dollar-quoted body, and in the
:: cast operator. A statement that needs the literal character rather
than a placeholder writes ??, which becomes a single ?. That matters
for PostgreSQL, where ? is a JSON containment operator.
Nothing here escapes a value or builds SQL out of one. Values are bound by the engine, always.
Constants
QUESTION
sql.QUESTION = 'question'
A bare ? for every parameter, in order. MySQL and MariaDB.
NUMBERED
sql.NUMBERED = 'numbered'
$1, $2 and so on, numbered from one in binding order. PostgreSQL.
INDEXED
sql.INDEXED = 'indexed'
?1, ?2 and so on. SQLite, which accepts a bare ? too but is given
explicit indices so a repeated named parameter can be bound once rather
than once per mention.
STYLES
sql.params.STYLES = [...]
Every style a driver may declare.
Functions
placeholder()
sql.placeholder(style: string, position: number) -> string
The placeholder text for the parameter at position, counting from one.
Used by the statement builders in crud, which generate SQL and so need
to spell placeholders themselves rather than translate them.
Parameters
style(string) — One ofQUESTION,NUMBEREDorINDEXED.position(number) — The parameter’s 1 based binding position.
Returns string
Raises QueryError if style is not one this module defines.
tokenize()
sql.params.tokenize(sql: string) -> list[dict]
Splits sql into the pieces that matter, in order.
Each token is a dictionary with a kind:
'text'is SQL to copy through,textholding it.'opaque'is a run nothing inside counts in: a string, a quoted or bracketed identifier, a comment, or a dollar-quoted body. Also copied through, but never looked inside.'question'is a positional placeholder.'named'is a named one,nameholding the name without its colon.'separator'is a semicolon at statement level.
Everything in this module reads the same statement the same way because everything in this module goes through here.
Parameters
sql(string)
Returns list[dict]
scan()
sql.scan(sql: string) -> dict
What placeholders sql uses, without needing any values.
prepare() needs this: it translates a statement before it has anything
to bind, so it has to know what the statement asks for.
Parameters
sql(string)
Returns dict — { positional, names }, positional the count of
? and names the :name names in first-mention order.
split_statements()
sql.split_statements(sql: string) -> list[string]
Splits a script into its statements.
Only semicolons outside strings, identifiers and comments separate, so a semicolon inside a string literal or a trigger body does not split the statement it belongs to. Empty pieces, which a trailing semicolon leaves behind, are dropped.
Parameters
sql(string)
Returns list[string]
translate()
sql.translate(sql: string, params, style: string) -> dict
Rewrites sql into style and puts the values in binding order.
Accepts two forms, and only one of them per statement:
- Positional.
?in the statement,paramsa list. Values bind in the order they appear. - Named.
:namein the statement,paramsa dictionary. A name used more than once binds once underNUMBEREDandINDEXED, and once per mention underQUESTION, which has no way to refer back.
Mixing the two in one statement raises, as does supplying a list for a named statement or a dictionary for a positional one. Each of those is a mistake with a silent wrong answer available, so none of them is guessed at.
Parameters
sql(string) — The statement, written with?or:name.params(list|dict|nil) — The values to bind.nilmeans none.style(string) — The active driver’s placeholder style.
Returns dict — { sql, values }, where values is always a list
in binding order.
Raises QueryError if the two forms are mixed, if params is the
wrong shape for the form used, if a positional statement and its list
disagree about how many values there are, or if a named statement
mentions a name the dictionary does not hold.
2026, Richard Ore and Zuri contributors
sql.pool
import sql.pool
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.pool.*needsimport sql.pool.
Keeping connections open and lending them out.
Opening a connection is expensive: for PostgreSQL it is a TCP connection, a TLS handshake and an authentication exchange before the first statement runs. A server that opened one per request would spend most of its time connecting. A pool opens a few and lends them out.
var db = sql.pool('postgres://localhost/app', { max: 10 })
var posts = db.fetch_all('select * from posts')
Used that way the pool takes a connection, runs the statement and gives
it back. Where several statements have to run on the same connection,
which a transaction requires, acquire() holds one until it is released
and with_connection() does the releasing.
Sizing
A pool wants to be as small as the work allows. Connections are not free at the other end either, and a pool larger than the database can usefully serve turns a queue in the application into a queue in the database, where it is harder to see.
SQLite wants a smaller pool than a server engine does. Readers are concurrent but writers are not, so past a handful of connections the writes queue on the database’s own lock rather than running. An in-memory SQLite database goes further: it belongs to the connection that opened it, so a pool of several would be several different empty databases. The pool clamps that case to one connection.
Isolates
A pool belongs to the isolate that made it. An isolate that needs database access makes its own.
Constants
DEFAULT_MAX
sql.pool.DEFAULT_MAX = 10
How many connections a pool opens at most, when it is not told.
DEFAULT_IDLE_TIMEOUT
sql.pool.DEFAULT_IDLE_TIMEOUT = 300000
How long an idle connection is kept before being closed, in milliseconds. Zero keeps them indefinitely.
DEFAULT_MAX_LIFETIME
sql.pool.DEFAULT_MAX_LIFETIME = 1800000
How long any connection is kept before being replaced, in milliseconds. Zero keeps them indefinitely.
Recycling connections on a schedule is what stops a pool holding one that a database restart, a failover or a firewall has quietly invalidated.
Classes
Pool
class sql.Pool
A pool of connections to one database.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.Pool(driver, options, settings)
Parameters
driver(Driver)options(dict) — The adapter’s connection options.settings(dict) — The pool’s own settings:min,max,idle_timeout,max_lifetime,validate_on_acquireandon_connect.
Pool.size()
sql.Pool.size() -> number
How many connections exist, lent out or not.
Returns number
Pool.available()
sql.Pool.available() -> number
How many are available right now.
Returns number
Pool.in_use()
sql.Pool.in_use() -> number
How many are currently lent out.
Returns number
Pool.stats()
sql.Pool.stats() -> dict
A snapshot of the pool: { size, idle, in_use, max }.
Returns dict
Pool.acquire()
sql.Pool.acquire() -> Connection
Takes a connection, waiting for one if they are all busy.
The caller has to give it back. close() on the connection does that,
which is what makes a pooled connection a drop-in for one that is not
pooled, and with_connection() does it whatever happens.
There is no waiting when the pool is empty, and no timeout to set. A pool belongs to one isolate, so nothing else can release a connection while this call is running; blocking would never end. Running out means the pool is too small for the work or a connection is being held longer than it should be, and both of those are better reported than waited on.
Returns Connection
Raises PoolExhaustedError if every connection is in use.
Raises ClosedError if the pool has been closed.
Pool.release()
sql.Pool.release(connection)
Gives a connection back.
Called for you by Connection.close() on a pooled connection, so there
is rarely a reason to call it directly.
A connection that has been closed for real, or that has outlived
max_lifetime, is discarded rather than kept.
Parameters
connection(Connection)
Pool.with_connection()
sql.Pool.with_connection(body: function) -> any
Runs body with a connection, giving it back whatever happens.
db.with_connection(@(connection) {
connection.transaction(@(tx) {
tx.exec('...')
})
})
Parameters
body(function) — Called with aConnection.
Returns any — Whatever body returned.
Pool.transaction()
sql.Pool.transaction(body: function, isolation) -> any
Runs body in a transaction on a connection of its own.
Parameters
body(function) — Called with aTransaction.isolation(string|nil)
Returns any — Whatever body returned.
Pool.query()
sql.Pool.query(sql: string, params) -> ResultSet
Runs a query on a connection from the pool.
Parameters
sql(string)params(list|dict|nil)
Returns ResultSet
Pool.exec()
sql.Pool.exec(sql: string, params) -> ExecResult
Runs a statement for its effect on a connection from the pool.
Parameters
sql(string)params(list|dict|nil)
Returns ExecResult
Pool.fetch_one()
sql.Pool.fetch_one(sql: string, params) -> dict|nil
The first row of a query, or nil.
Parameters
sql(string)params(list|dict|nil)
Returns dict|nil
Pool.fetch_all()
sql.Pool.fetch_all(sql: string, params) -> list[dict]
Every row of a query.
Parameters
sql(string)params(list|dict|nil)
Returns list[dict]
Pool.fetch_value()
sql.Pool.fetch_value(sql: string, params, fallback) -> any
The first column of the first row.
Parameters
sql(string)params(list|dict|nil)fallback(any)
Returns any
Pool.close()
sql.Pool.close()
Closes every connection and refuses further use.
Connections currently lent out are closed as they come back.
Safe to call more than once.
Pool.is_closed()
sql.Pool.is_closed() -> bool
Whether this pool has been closed.
Returns bool
Pool.to_string()
sql.Pool.to_string()
2026, Richard Ore and Zuri contributors
sql.postgres
import sql.postgres
sqldoes not re-export this module, so it is reached only by importing it directly.
The PostgreSQL adapter.
Reach it through sql, which is what keeps a program portable:
import sql
var db = sql.open('postgres://alice:secret@localhost/app')
The module underneath is here for the parts of PostgreSQL that have no equivalent elsewhere: LISTEN and NOTIFY, the server’s own settings, and the type registry, which is how a program teaches the adapter about a type its database defines.
What this adapter speaks
Version 3 of the wire protocol, over TCP, optionally under TLS, or over
a unix domain socket. A host beginning with a slash is a socket
directory, as libpq spells it, and the socket inside it is named after
the port. TLS is neither offered nor wanted there, since nothing sits in
between, so sslmode is ignored.
Statements go through the extended protocol, which compiles them on the server and binds values rather than substituting them. Values travel in the server’s own binary form wherever this adapter has a codec for the type, and as text otherwise, so a type it has never heard of still arrives readable.
Submodules
| Module | Reached as | Summary |
|---|---|---|
sql.postgres.auth | import sql.postgres.auth | The authentication exchanges a PostgreSQL server can ask for. |
sql.postgres.connection | sql.postgres.connection.* | A PostgreSQL connection, as the layer above the adapters sees one. |
sql.postgres.cursor | sql.postgres.cursor.* | Reading a PostgreSQL result a batch at a time. |
sql.postgres.driver | sql.postgres.driver.* | Opening PostgreSQL connections: reading a connection string, establishing the socket, upgrading it to TLS… |
sql.postgres.errors | sql.postgres.errors.* | Turning PostgreSQL’s SQLSTATE codes into the shared error classes. |
sql.postgres.messages | import sql.postgres.messages | Framing for the PostgreSQL wire protocol, version 3. |
sql.postgres.notify | sql.postgres.notify.* | LISTEN and NOTIFY, PostgreSQL’s own publish and subscribe. |
sql.postgres.protocol | sql.postgres.protocol.* | The PostgreSQL frontend/backend protocol, version 3. |
sql.postgres.results | sql.postgres.results.* | Reading what the server says about a result it has just produced. |
sql.postgres.schema | import sql.postgres.schema | Introspection for PostgreSQL, through the standard information_schema views and the catalogue behind them. |
sql.postgres.statement | sql.postgres.statement.* | A PostgreSQL statement compiled once and run many times. |
sql.postgres.types | sql.postgres.types.* | Encoding and decoding PostgreSQL’s binary value formats. |
Constants
DRIVER
sql.postgres.DRIVER
The driver instance sql registers for this engine.
Functions
connect()
sql.postgres.connect(dsn) -> PostgresConnection
Opens a PostgreSQL connection directly, without going through sql.
Anything meant to stay portable should use sql.open() instead.
Parameters
dsn(string|dict) — A connection string or options.
Returns PostgresConnection
2026, Richard Ore and Zuri contributors
sql.postgres.auth
import sql.postgres.auth
sqldoes not re-export this module, so it is reached only by importing it directly.
The authentication exchanges a PostgreSQL server can ask for.
Which one happens is the server’s choice, made from its pg_hba.conf. A
modern server asks for SCRAM-SHA-256; older ones ask for MD5, which
PostgreSQL itself now discourages; and a server set to trust asks for
nothing at all.
Constants
SCRAM_SHA_256
sql.postgres.auth.SCRAM_SHA_256 = 'SCRAM-SHA-256'
The mechanism this adapter implements. Channel binding is not offered,
so the server sees plain SCRAM-SHA-256 rather than the -PLUS variant.
Functions
md5_response()
sql.postgres.auth.md5_response(user: string, password: string, salt) -> string
Builds the response to an MD5 password request.
The scheme is md5(md5(password + user) + salt), hex encoded, with a
literal md5 in front. It is weak and PostgreSQL says so; it is here
because servers still ask for it.
Parameters
user(string)password(string)salt(bytes) — The four bytes the server sent.
Returns string
nonce()
sql.postgres.auth.nonce() -> string
A fresh SCRAM nonce.
It has to be unpredictable: the nonce is what stops a recorded exchange
being replayed, so it comes from the system’s random source rather than
from rand().
Returns string
parse_scram()
sql.postgres.auth.parse_scram(message: string) -> dict
Splits a SCRAM message into its key=value parts.
Parameters
message(string)
Returns dict
offers_scram()
sql.postgres.auth.offers_scram(payload) -> bool
Whether the server offered SCRAM-SHA-256 among its mechanisms.
Parameters
payload(bytes) — The mechanism list, zero-terminated names.
Returns bool
client_first()
sql.postgres.auth.client_first(client_nonce: string) -> dict
The client’s opening SCRAM message.
The username is left empty, which is what PostgreSQL expects: it takes the user from the startup message and ignores this one.
Parameters
client_nonce(string)
Returns dict — { bare, full }, the second with the channel
binding header the exchange is signed over.
client_proof()
sql.postgres.auth.client_proof(password: string, client_nonce: string, client_first_bare: string, server_first: string) -> dict
Works out the client’s proof from the server’s challenge.
This is SCRAM as RFC 5802 defines it: the password is salted and iterated into a salted password, which yields a client key, whose hash the server already holds. Signing the whole exchange with that hash and returning the signature XORed with the client key proves the password without sending it.
Parameters
password(string)client_nonce(string) — The nonce sent in the first message.client_first_bare(string) — The first message without its header.server_first(string) — The server’s reply, verbatim.
Returns dict — { message, server_signature }, the message to
send and the signature the server’s own reply has to match.
Raises AuthenticationError if the server’s reply is malformed or
its nonce does not extend the client’s.
verify_server()
sql.postgres.auth.verify_server(server_final: string, expected: string)
Checks the server’s closing message really is from a server that knows the password.
Skipping this would leave the exchange one-way, proving the client to the server but not the server to the client.
Parameters
server_final(string)expected(string) — Fromclient_proof().
Raises AuthenticationError if the signature is absent or wrong.
2026, Richard Ore and Zuri contributors
sql.postgres.connection
import sql.postgres.connection
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.postgres.connection.*needsimport sql.postgres.connection.
A PostgreSQL connection, as the layer above the adapters sees one.
Statements are compiled once
Every statement run here goes through the extended protocol, which compiles it on the server and then executes the compiled form. The compiled form is kept, keyed by the statement’s text, so a statement run in a loop is compiled once. That is also what makes binary parameters and results possible: both need to know the types, and compiling is what reports them.
Cursors need a transaction
A PostgreSQL portal is destroyed when its transaction ends, and a statement outside a transaction is its own transaction. So a cursor opened outside one would be closed by the server before the first row was read. This opens a transaction for the cursor’s own sake in that case and commits it when the cursor closes.
Constants
CACHE_LIMIT
sql.postgres.connection.CACHE_LIMIT = 64
How many compiled statements a connection keeps.
Classes
PostgresConnection
class sql.postgres.PostgresConnection < DriverConnection
An open PostgreSQL connection.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.postgres.PostgresConnection(driver, protocol, options)
Parameters
driver(Driver)protocol(Protocol) — An authenticated protocol.options(dict)
PostgresConnection.protocol()
sql.postgres.PostgresConnection.protocol() -> Protocol
The protocol underneath, for the PostgreSQL-only features that are not part of the shared contract.
Returns Protocol
PostgresConnection.parameters()
sql.postgres.PostgresConnection.parameters() -> dict
The settings the server reported when the connection opened.
Returns dict
PostgresConnection.schema_adapter()
sql.postgres.PostgresConnection.schema_adapter() -> PostgresSchema
This connection’s introspection.
Returns PostgresSchema
PostgresConnection.execute()
sql.postgres.PostgresConnection.execute(sql: string, values: list) -> dict
Runs a statement for its effect.
Parameters
sql(string)values(list)
Returns dict — { rows_affected, last_insert_id }
PostgresConnection.select()
sql.postgres.PostgresConnection.select(sql: string, values: list) -> dict
Runs a statement and reads its whole result.
Parameters
sql(string)values(list)
Returns dict — { columns, rows }
PostgresConnection.open_cursor()
sql.postgres.PostgresConnection.open_cursor(sql: string, values: list, options: dict) -> PostgresCursor
Runs a statement and hands back a cursor over its result.
Parameters
sql(string)values(list)options(dict) —{ batch }, how many rows to fetch at a time. Defaults to 256.
Returns PostgresCursor
PostgresConnection.prepare()
sql.postgres.PostgresConnection.prepare(sql: string) -> PostgresStatement
Compiles a statement for repeated use.
Parameters
sql(string)
Returns PostgresStatement
PostgresConnection.begin()
sql.postgres.PostgresConnection.begin(isolation)
Opens a transaction, or takes a savepoint when one is open.
Parameters
isolation(string|nil)
PostgresConnection.commit()
sql.postgres.PostgresConnection.commit()
Commits, or releases the innermost savepoint.
PostgresConnection.rollback()
sql.postgres.PostgresConnection.rollback()
Rolls back, or undoes the innermost savepoint.
PostgresConnection.savepoint()
sql.postgres.PostgresConnection.savepoint(name: string)
PostgresConnection.release_savepoint()
sql.postgres.PostgresConnection.release_savepoint(name: string)
PostgresConnection.rollback_to()
sql.postgres.PostgresConnection.rollback_to(name: string)
PostgresConnection.in_transaction()
sql.postgres.PostgresConnection.in_transaction() -> bool
Whether a transaction is open.
The server reports this with every result, so this is what it last said rather than a guess kept on this side.
Returns bool
PostgresConnection.in_failed_transaction()
sql.postgres.PostgresConnection.in_failed_transaction() -> bool
Whether the open transaction has failed and can now only be rolled back, which is the state PostgreSQL puts one in after an error.
Returns bool
PostgresConnection.last_insert_id()
sql.postgres.PostgresConnection.last_insert_id()
Always raises: PostgreSQL does not report the id of an inserted row.
Raises NotSupportedError always.
PostgresConnection.ping()
sql.postgres.PostgresConnection.ping() -> bool
Checks the connection is usable.
Returns bool
PostgresConnection.server_version()
sql.postgres.PostgresConnection.server_version() -> string
The server’s version, such as '17.2'.
Returns string
PostgresConnection.listener()
sql.postgres.PostgresConnection.listener() -> Listener
A listener for LISTEN and NOTIFY on this connection.
var listener = db.native().listener()
listener.listen('jobs')
Returns Listener
PostgresConnection.notifications()
sql.postgres.PostgresConnection.notifications() -> list[dict]
Notifications received on this connection since the last call.
Returns list[dict] — Each { channel, payload, pid }.
PostgresConnection.close()
sql.postgres.PostgresConnection.close()
Closes the connection. Safe to call more than once.
PostgresConnection.is_closed()
sql.postgres.PostgresConnection.is_closed()
PostgresConnection.to_string()
sql.postgres.PostgresConnection.to_string()
2026, Richard Ore and Zuri contributors
sql.postgres.cursor
import sql.postgres.cursor
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.postgres.cursor.*needsimport sql.postgres.cursor.
Reading a PostgreSQL result a batch at a time.
The server holds the result in a portal and sends as many rows as each Execute asks for. When more remain it says so, and the next Execute continues where the last stopped. Nothing but the current batch is ever held on this side.
Classes
PostgresCursor
class sql.postgres.PostgresCursor < DriverCursor
A portal being read.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.postgres.PostgresCursor(connection, state)
Parameters
connection(PostgresConnection)state(dict) — The portal’s name, its columns, the first batch of rows, whether more remain, and whether this cursor opened the transaction it is running in.
PostgresCursor.columns()
sql.postgres.PostgresCursor.columns()
PostgresCursor.next_row()
sql.postgres.PostgresCursor.next_row() -> list|nil
The next row as a list of values, or nil once the result is finished.
Returns list|nil
PostgresCursor.close()
sql.postgres.PostgresCursor.close()
Releases the portal, and the transaction if this cursor opened one. Safe to call more than once.
PostgresCursor.is_closed()
sql.postgres.PostgresCursor.is_closed() -> bool
Whether the cursor has been closed or read to the end.
Returns bool
PostgresCursor.to_string()
sql.postgres.PostgresCursor.to_string()
2026, Richard Ore and Zuri contributors
sql.postgres.driver
import sql.postgres.driver
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.postgres.driver.*needsimport sql.postgres.driver.
Opening PostgreSQL connections: reading a connection string, establishing the socket, upgrading it to TLS when asked, and authenticating.
Constants
DEFAULT_PORT
sql.postgres.DEFAULT_PORT = 5432
The port a PostgreSQL server listens on unless told otherwise.
DEFAULT_TIMEOUT
sql.postgres.driver.DEFAULT_TIMEOUT = 30000
How long to wait for the socket and for each read, in milliseconds.
MAX_PARAMETERS
sql.postgres.MAX_PARAMETERS = 65535
Most parameters one statement can bind. The protocol counts them in a signed 16 bit field, so this is a hard limit rather than a configured one.
SSL_MODES
sql.postgres.SSL_MODES = [...]
What sslmode can be.
disable never uses TLS. prefer uses it when the server offers it and
carries on without when it does not. require insists, and fails when
the server refuses.
Classes
PostgresDriver
class sql.postgres.PostgresDriver < Driver
Opens PostgreSQL connections.
PostgresDriver.name()
sql.postgres.PostgresDriver.name()
PostgresDriver.schemes()
sql.postgres.PostgresDriver.schemes()
PostgresDriver.capabilities()
sql.postgres.PostgresDriver.capabilities() -> dict
What PostgreSQL can do.
last_insert_id is false, which is the one difference most visible from
above: an insert that wants its id back gets a RETURNING clause
instead, which Connection.insert() adds.
Returns dict
PostgresDriver.parse_dsn()
sql.postgres.PostgresDriver.parse_dsn(dsn) -> dict
Reads a connection string, in either of the two forms PostgreSQL’s own tools accept.
A URI:
postgres://alice:secret@db.example.com:5432/app?sslmode=require
postgresql://localhost/app
Or keywords:
host=localhost port=5432 dbname=app user=alice password=secret
Anything absent is filled in the way libpq fills it: the user defaults
to postgres, the database to the user’s name, the host to localhost
and the port to 5432.
Parameters
dsn(string|dict)
Returns dict
Raises ConnectionError if the string cannot be read.
PostgresDriver.connect()
sql.postgres.PostgresDriver.connect(options: dict) -> PostgresConnection
Opens the connection and authenticates.
Parameters
options(dict) — Fromparse_dsn().
Returns PostgresConnection
2026, Richard Ore and Zuri contributors
sql.postgres.errors
import sql.postgres.errors
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.postgres.errors.*needsimport sql.postgres.errors.
Turning PostgreSQL’s SQLSTATE codes into the shared error classes.
Every error PostgreSQL reports carries a five character SQLSTATE. The
first two characters are its class and the rest narrow it down, so
23505 is class 23, integrity constraint violation, specifically a
unique violation. Mapping works on the specific code where one is worth
telling apart and falls back to the class otherwise, which means a code
this table has never seen still lands somewhere sensible.
Constants
SPECIFIC
sql.postgres.errors.SPECIFIC = {...}
Codes specific enough to name a class of their own.
CLASSES
sql.postgres.errors.CLASSES = {...}
SQLSTATE classes, for codes the table above does not name.
Functions
class_for()
sql.postgres.class_for(sqlstate) -> class
The class that best describes sqlstate.
Parameters
sqlstate(string|nil)
Returns class
raise_for()
sql.postgres.raise_for(report: dict, query)
Raises the right class for a server error report.
Parameters
report(dict) — The fields the server sent, asprotocol.parse_error()returns them.query(string|nil) — The statement that was running.
Raises SqlError always.
2026, Richard Ore and Zuri contributors
sql.postgres.messages
import sql.postgres.messages
sqldoes not re-export this module, so it is reached only by importing it directly.
Framing for the PostgreSQL wire protocol, version 3.
Every message is a one byte tag, a four byte big-endian length that counts itself, and then the body. The one exception is the startup message, which has no tag because the server has not yet been told which protocol version is in use.
Constants
AUTHENTICATION
sql.postgres.messages.AUTHENTICATION = 'R'
Messages the server sends, by tag.
BACKEND_KEY_DATA
sql.postgres.messages.BACKEND_KEY_DATA = 'K'
BIND_COMPLETE
sql.postgres.messages.BIND_COMPLETE = '2'
CLOSE_COMPLETE
sql.postgres.messages.CLOSE_COMPLETE = '3'
COMMAND_COMPLETE
sql.postgres.messages.COMMAND_COMPLETE = 'C'
COPY_IN_RESPONSE
sql.postgres.messages.COPY_IN_RESPONSE = 'G'
COPY_OUT_RESPONSE
sql.postgres.messages.COPY_OUT_RESPONSE = 'H'
COPY_DATA
sql.postgres.messages.COPY_DATA = 'd'
COPY_DONE
sql.postgres.messages.COPY_DONE = 'c'
DATA_ROW
sql.postgres.messages.DATA_ROW = 'D'
EMPTY_QUERY_RESPONSE
sql.postgres.messages.EMPTY_QUERY_RESPONSE = 'I'
ERROR_RESPONSE
sql.postgres.messages.ERROR_RESPONSE = 'E'
NO_DATA
sql.postgres.messages.NO_DATA = 'n'
NOTICE_RESPONSE
sql.postgres.messages.NOTICE_RESPONSE = 'N'
NOTIFICATION_RESPONSE
sql.postgres.messages.NOTIFICATION_RESPONSE = 'A'
PARAMETER_DESCRIPTION
sql.postgres.messages.PARAMETER_DESCRIPTION = 't'
PARAMETER_STATUS
sql.postgres.messages.PARAMETER_STATUS = 'S'
PARSE_COMPLETE
sql.postgres.messages.PARSE_COMPLETE = '1'
PORTAL_SUSPENDED
sql.postgres.messages.PORTAL_SUSPENDED = 's'
READY_FOR_QUERY
sql.postgres.messages.READY_FOR_QUERY = 'Z'
ROW_DESCRIPTION
sql.postgres.messages.ROW_DESCRIPTION = 'T'
AUTH_OK
sql.postgres.messages.AUTH_OK = 0
Authentication requests, by the number that follows the tag.
AUTH_CLEARTEXT
sql.postgres.messages.AUTH_CLEARTEXT = 3
AUTH_MD5
sql.postgres.messages.AUTH_MD5 = 5
AUTH_SASL
sql.postgres.messages.AUTH_SASL = 10
AUTH_SASL_CONTINUE
sql.postgres.messages.AUTH_SASL_CONTINUE = 11
AUTH_SASL_FINAL
sql.postgres.messages.AUTH_SASL_FINAL = 12
PROTOCOL_VERSION
sql.postgres.messages.PROTOCOL_VERSION = 196608
The protocol version this adapter speaks: 3.0, as a single number with the major version in the high half.
SSL_REQUEST
sql.postgres.messages.SSL_REQUEST = 80877103
The number the server recognises as a request to start TLS. It takes the place of a protocol version in a startup-shaped message.
Functions
read_cstring()
sql.postgres.messages.read_cstring(payload, at: number) -> dict
Splits a run of zero-terminated strings.
Several messages are built this way, including the error report, where each field is a one byte code followed by its text.
Parameters
payload(bytes)at(number) — Where to start.
Returns dict — { value, next }
Classes
Writer
class sql.postgres.messages.Writer
Builds one outgoing message.
Bytes accumulate in a list and frame() puts the tag and length on the
front, since the length cannot be known until the body is finished.
Writer.byte()
sql.postgres.messages.Writer.byte(value: number) -> Writer
Appends one byte.
Parameters
value(number)
Returns Writer — This writer, so calls chain.
Writer.int16()
sql.postgres.messages.Writer.int16(value: number) -> Writer
Appends a big-endian 16 bit integer.
Parameters
value(number)
Returns Writer
Writer.int32()
sql.postgres.messages.Writer.int32(value: number) -> Writer
Appends a big-endian 32 bit integer.
Parameters
value(number)
Returns Writer
Writer.cstring()
sql.postgres.messages.Writer.cstring(value: string) -> Writer
Appends a string and the zero byte that ends it.
Parameters
value(string)
Returns Writer
Writer.raw()
sql.postgres.messages.Writer.raw(values: list) -> Writer
Appends raw bytes.
Parameters
values(list)
Returns Writer
Writer.length()
sql.postgres.messages.Writer.length() -> number
How many bytes the body holds so far.
Returns number
Writer.frame()
sql.postgres.messages.Writer.frame(tag) -> bytes
The finished message, ready to send.
Parameters
tag(string|nil) — The message’s tag, or nil for a startup message, which has none.
Returns bytes
Reader
class sql.postgres.messages.Reader
Reads framed messages from a connected stream.
Works over a plain TcpStream or a TlsStream without caring which,
since both read the same way.
Constructor
sql.postgres.messages.Reader(stream)
Parameters
stream(TcpStream|TlsStream) — An already-connected stream.
Reader.upgrade()
sql.postgres.messages.Reader.upgrade(stream)
Swaps the stream, for the point in the handshake where a plain connection becomes a TLS one.
Parameters
stream(TlsStream)
Reader.read_exact()
sql.postgres.messages.Reader.read_exact(count: number) -> bytes
Reads exactly count bytes, waiting for as many as it takes.
Parameters
count(number)
Returns bytes
Raises ConnectionError if the server closes first.
Reader.read_message()
sql.postgres.messages.Reader.read_message() -> dict
Reads the next message.
Returns dict — { tag, payload }
2026, Richard Ore and Zuri contributors
sql.postgres.notify
import sql.postgres.notify
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.postgres.notify.*needsimport sql.postgres.notify.
LISTEN and NOTIFY, PostgreSQL’s own publish and subscribe.
A connection that has run LISTEN channel receives a message whenever
anything runs NOTIFY channel, including from another connection or
another process entirely. It is the one part of this adapter with no
equivalent in SQLite, so it lives here rather than in the shared
contract and is reached through db.native().
var listener = db.native().listener()
listener.listen('jobs')
while true {
for message in listener.poll() {
handle(message.payload)
}
}
Notifications arrive between other messages, so a connection that is
busy running statements collects them as it goes and poll() hands over
whatever has accumulated. A connection doing nothing else has to ask,
which is what wait() does.
Classes
Listener
class sql.postgres.Listener
Subscribes a connection to channels and collects what arrives.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.postgres.Listener(connection)
Parameters
connection(PostgresConnection)
Listener.channels()
sql.postgres.Listener.channels() -> list[string]
The channels this listener is subscribed to.
Returns list[string]
Listener.listen()
sql.postgres.Listener.listen(channel: string)
Subscribes to channel.
Parameters
channel(string)
Listener.unlisten()
sql.postgres.Listener.unlisten(channel: string)
Unsubscribes from channel.
Parameters
channel(string)
Listener.notify()
sql.postgres.Listener.notify(channel: string, payload)
Sends a notification.
The payload is bound rather than pasted in, so it can hold anything including quotes.
Parameters
channel(string)payload(string|nil)
Listener.poll()
sql.postgres.Listener.poll() -> list[dict]
Whatever has arrived since the last call, without waiting.
Returns list[dict] — Each { channel, payload, pid }.
Listener.wait()
sql.postgres.Listener.wait(rounds) -> list[dict]
Waits for at least one notification, checking the connection each time round.
There is no way to block on a socket and a database at once here, so this asks the server a trivial question on each pass, which is what carries any pending notification back with it.
Parameters
rounds(number|nil) — How many times to check before giving up. Unlimited when nil.
Returns list[dict] — The notifications received, which is empty
only if rounds ran out.
Listener.close()
sql.postgres.Listener.close()
Unsubscribes from everything. Safe to call more than once.
Listener.to_string()
sql.postgres.Listener.to_string()
2026, Richard Ore and Zuri contributors
sql.postgres.protocol
import sql.postgres.protocol
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.postgres.protocol.*needsimport sql.postgres.protocol.
The PostgreSQL frontend/backend protocol, version 3.
A connection is a sequence of messages in both directions. The client sends a request and then a Sync, and the server answers with some number of messages ending in ReadyForQuery. Everything between those two points belongs to the request, and nothing else may be sent until it arrives, which is what makes this a state machine rather than a set of independent calls.
Why statements are described before they are bound
A value can be sent and received either as text or in the server’s own binary form, chosen per parameter and per column, and the choice is made in the Bind message. To choose well, the types have to be known first, which is what Describe reports. So a statement is parsed and described once, and every execution after that reuses what was learned. A statement run in a loop pays for this once.
2026, Richard Ore and Zuri contributors
sql.postgres.results
import sql.postgres.results
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.postgres.results.*needsimport sql.postgres.results.
Reading what the server says about a result it has just produced.
These live apart from the connection so the statement can use them too. A statement that reached for them through the connection, which already reaches for the statement, would be a circular import.
Functions
affected_rows()
sql.postgres.affected_rows(tag) -> number
The number of rows a command tag reports.
The tag is the command’s name followed by its counts. INSERT 0 3
carries an unused OID before the row count and everything else carries
the count alone, so the last field is the one to read. A tag with no
count at all, as CREATE TABLE has, means no rows.
Parameters
tag(string)
Returns number
type_name_of()
sql.postgres.type_name_of(oid: number) -> string
The name of the type an OID stands for, or the OID as text for one this adapter has no name for, which is what an extension’s type will be.
Parameters
oid(number)
Returns string
describe_columns()
sql.postgres.describe_columns(columns: list) -> list[dict]
Reduces the server’s column descriptors to the two fields the shared contract asks for.
Parameters
columns(list)
Returns list[dict]
2026, Richard Ore and Zuri contributors
sql.postgres.schema
import sql.postgres.schema
sqldoes not re-export this module, so it is reached only by importing it directly.
Introspection for PostgreSQL, through the standard information_schema
views and the catalogue behind them.
information_schema covers tables, views and columns and is the SQL
standard, so those queries would work on other engines too. Constraints
and indexes are read from pg_catalog instead, because the standard
views describe them in a form that loses which columns an index actually
covers and in what order.
Where a caller names no schema, the server’s own search_path decides,
which is what makes an unqualified table name mean the same here as it
does in a query.
Classes
PostgresSchema
class sql.postgres.schema.PostgresSchema < SchemaAdapter
Answers introspection questions about a PostgreSQL database.
Constructor
sql.postgres.schema.PostgresSchema(connection)
Parameters
connection(PostgresConnection)
PostgresSchema.tables()
sql.postgres.schema.PostgresSchema.tables(schema) -> list[string]
The tables the application created.
Parameters
schema(string|nil) — The schema to look in, or nil for whateversearch_pathresolves to.
Returns list[string]
PostgresSchema.views()
sql.postgres.schema.PostgresSchema.views(schema) -> list[string]
The views.
Parameters
schema(string|nil)
Returns list[string]
PostgresSchema.columns()
sql.postgres.schema.PostgresSchema.columns(table: string, schema) -> list[dict]
The columns of table, in order.
Parameters
table(string)schema(string|nil)
Returns list[dict]
PostgresSchema.primary_key_columns()
sql.postgres.schema.PostgresSchema.primary_key_columns(table: string, schema) -> list[string]
The primary key columns, in key order.
pg_index records the key’s own column order, which is not the table’s,
so the order here is the one the index was declared with.
Parameters
table(string)schema(string|nil)
Returns list[string]
PostgresSchema.indexes()
sql.postgres.schema.PostgresSchema.indexes(table: string, schema) -> list[dict]
The indexes on table, each { name, columns, unique }.
Parameters
table(string)schema(string|nil)
Returns list[dict]
PostgresSchema.foreign_keys()
sql.postgres.schema.PostgresSchema.foreign_keys(table: string, schema) -> list[dict]
The foreign keys declared on table.
Parameters
table(string)schema(string|nil)
Returns list[dict]
2026, Richard Ore and Zuri contributors
sql.postgres.statement
import sql.postgres.statement
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.postgres.statement.*needsimport sql.postgres.statement.
A PostgreSQL statement compiled once and run many times.
Every statement this adapter runs is compiled on the server, so a prepared statement is not doing anything a plain query does not. The difference is ownership: this one has a name of its own and is released when the program says so, rather than living in the connection’s cache and being evicted when the cache fills.
Classes
PostgresStatement
class sql.postgres.PostgresStatement < DriverStatement
A compiled PostgreSQL statement.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.postgres.PostgresStatement(connection, compiled)
Parameters
connection(PostgresConnection)compiled(dict) —name,sql,parametersandcolumnsas the server described them.
PostgresStatement.columns()
sql.postgres.PostgresStatement.columns()
PostgresStatement.parameter_count()
sql.postgres.PostgresStatement.parameter_count()
PostgresStatement.execute()
sql.postgres.PostgresStatement.execute(values: list) -> dict
Runs the statement for its effect.
Parameters
values(list)
Returns dict — { rows_affected, last_insert_id }
PostgresStatement.select()
sql.postgres.PostgresStatement.select(values: list) -> dict
Runs the statement and reads its whole result.
Parameters
values(list)
Returns dict — { columns, rows }
PostgresStatement.open_cursor()
sql.postgres.PostgresStatement.open_cursor(values: list, options: dict) -> PostgresCursor
Runs the statement and hands back a cursor over its result.
Parameters
values(list)options(dict) —{ batch }
Returns PostgresCursor
PostgresStatement.close()
sql.postgres.PostgresStatement.close()
Releases the statement on the server. Safe to call more than once.
PostgresStatement.is_closed()
sql.postgres.PostgresStatement.is_closed() -> bool
Whether this statement has been closed.
Returns bool
PostgresStatement.to_string()
sql.postgres.PostgresStatement.to_string()
2026, Richard Ore and Zuri contributors
sql.postgres.types
import sql.postgres.types
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.postgres.types.*needsimport sql.postgres.types.
Encoding and decoding PostgreSQL’s binary value formats.
PostgreSQL will send a value either as text or as the server’s own
binary representation, chosen per column. Binary is what this adapter
asks for wherever it has a decoder, because it is exact: a float8
arrives as the same eight bytes the server holds rather than as a
decimal rendering that has to be parsed back, and a numeric arrives as
its actual digits rather than as text that a double would round.
Where there is no decoder, text is asked for instead and the value arrives as a string. That is the important half of the design: a type this adapter has never heard of, including one an extension added, still comes back as something readable rather than as bytes nobody can interpret.
Constants
OIDS
sql.postgres.OIDS = {...}
PostgreSQL’s built-in type OIDs, by name.
An array type has an OID of its own, listed here as <Type>Array.
ARRAY_ELEMENTS
sql.postgres.types.ARRAY_ELEMENTS = {...}
The element type each array OID holds.
EPOCH_OFFSET
sql.postgres.types.EPOCH_OFFSET = 946684800
Seconds between the Unix epoch and PostgreSQL’s, which is 2000-01-01 rather than 1970-01-01.
DEFAULT_REGISTRY
sql.postgres.DEFAULT_REGISTRY
The registry every connection uses unless given one of its own.
Functions
read_int16()
sql.postgres.types.read_int16(buffer, at: number) -> number
Reads a signed 16 bit big-endian integer.
Parameters
buffer(bytes)at(number)
Returns number
read_uint16()
sql.postgres.types.read_uint16(buffer, at: number) -> number
Reads an unsigned 16 bit big-endian integer.
Parameters
buffer(bytes)at(number)
Returns number
read_int32()
sql.postgres.types.read_int32(buffer, at: number) -> number
Reads a signed 32 bit big-endian integer.
Parameters
buffer(bytes)at(number)
Returns number
read_uint32()
sql.postgres.types.read_uint32(buffer, at: number) -> number
Reads an unsigned 32 bit big-endian integer.
Parameters
buffer(bytes)at(number)
Returns number
read_int64()
sql.postgres.types.read_int64(buffer, at: number) -> number
Reads a signed 64 bit big-endian integer.
Built from the two halves rather than shifted in one go, because the top bits of a 64 bit value do not survive a bitwise shift on a double.
Parameters
buffer(bytes)at(number)
Returns number
write_int16()
sql.postgres.types.write_int16(value: number) -> list[number]
Writes a signed 16 bit big-endian integer.
Parameters
value(number)
Returns list[number]
write_int32()
sql.postgres.types.write_int32(value: number) -> list[number]
Writes a signed 32 bit big-endian integer.
Parameters
value(number)
Returns list[number]
write_int64()
sql.postgres.types.write_int64(value: number) -> list[number]
Writes a signed 64 bit big-endian integer.
Parameters
value(number)
Returns list[number]
text_of()
sql.postgres.types.text_of(buffer) -> string
A bytes slice as a UTF-8 string.
Parameters
buffer(bytes)
Returns string
decode_numeric()
sql.postgres.types.decode_numeric(buffer) -> Decimal|number
Decodes PostgreSQL’s numeric into an exact Decimal.
The wire form is a count of base 10000 digits, the position of the first
of them relative to the decimal point, a sign, the number of places to
display, and then the digits. Reassembling it as text and handing that
to Decimal keeps every digit; going through a double anywhere in here
would defeat the point of the type.
Parameters
buffer(bytes)
Returns Decimal|number
encode_numeric()
sql.postgres.types.encode_numeric(value) -> list[number]
Encodes a Decimal, a number or a bigint as numeric.
Parameters
value(Decimal|number|bigint)
Returns list[number]
date_from_micros()
sql.postgres.types.date_from_micros(micros: number) -> date.Date
Turns a count of microseconds since the PostgreSQL epoch into a
date.Date in UTC.
Parameters
micros(number)
Returns date.Date
micros_from_date()
sql.postgres.types.micros_from_date(moment) -> number
The microseconds since the PostgreSQL epoch a date.Date stands for.
Parameters
moment(date.Date)
Returns number
decode_bool()
sql.postgres.types.decode_bool(buffer, _oid)
A decoder reads one column value from its binary form.
Each takes the value’s bytes and the OID it arrived under, and returns a Zuri value.
decode_int2()
sql.postgres.types.decode_int2(buffer, _oid)
decode_int4()
sql.postgres.types.decode_int4(buffer, _oid)
decode_int8()
sql.postgres.types.decode_int8(buffer, _oid)
decode_oid()
sql.postgres.types.decode_oid(buffer, _oid)
decode_float4()
sql.postgres.types.decode_float4(buffer, _oid)
decode_float8()
sql.postgres.types.decode_float8(buffer, _oid)
decode_text()
sql.postgres.types.decode_text(buffer, _oid)
decode_bytea()
sql.postgres.types.decode_bytea(buffer, _oid)
decode_json()
sql.postgres.types.decode_json(buffer, _oid)
decode_jsonb()
sql.postgres.types.decode_jsonb(buffer, _oid)
jsonb carries a version byte in front of the text, which every server
so far sets to 1.
decode_uuid()
sql.postgres.types.decode_uuid(buffer, _oid)
decode_numeric_value()
sql.postgres.types.decode_numeric_value(buffer, _oid)
decode_date()
sql.postgres.types.decode_date(buffer, _oid)
date is a count of days from 2000-01-01, with no time part.
decode_time()
sql.postgres.types.decode_time(buffer, _oid)
time is microseconds since midnight, with no date part. It comes back
as a dictionary rather than a date.Date, because a time of day is not
a point in time and giving it an arbitrary date would be inventing
information.
decode_timetz()
sql.postgres.types.decode_timetz(buffer, _oid)
timetz is a time followed by its offset in seconds west of UTC.
decode_timestamp()
sql.postgres.types.decode_timestamp(buffer, _oid)
Both timestamp and timestamptz are microseconds from the PostgreSQL
epoch.
The server holds a timestamptz in UTC and converts on the way in and
out, so what arrives is already UTC and is handed back as such. A
timestamp has no zone at all; it is read as UTC because a date.Date
has to say something, and UTC is the only answer that does not invent a
zone the value never had.
decode_interval()
sql.postgres.types.decode_interval(buffer, _oid)
interval is a span rather than a point, so it comes back as its parts.
Months and days are kept separate from the time because neither has a
fixed length: a month is 28 to 31 days, and a day across a daylight
saving change is not 24 hours.
decode_inet()
sql.postgres.types.decode_inet(buffer, _oid)
inet and cidr share a form: address family, prefix bits, a flag
saying which of the two it is, then the address bytes.
decode_macaddr()
sql.postgres.types.decode_macaddr(buffer, _oid)
decode_point()
sql.postgres.types.decode_point(buffer, _oid)
decode_lseg()
sql.postgres.types.decode_lseg(buffer, _oid)
decode_box()
sql.postgres.types.decode_box(buffer, _oid)
decode_circle()
sql.postgres.types.decode_circle(buffer, _oid)
encode_bool()
sql.postgres.types.encode_bool(value)
An encoder turns a Zuri value into the bytes of its binary form.
encode_int2()
sql.postgres.types.encode_int2(value)
encode_int4()
sql.postgres.types.encode_int4(value)
encode_int8()
sql.postgres.types.encode_int8(value)
encode_float4()
sql.postgres.types.encode_float4(value)
encode_float8()
sql.postgres.types.encode_float8(value)
encode_text()
sql.postgres.types.encode_text(value)
encode_bytea()
sql.postgres.types.encode_bytea(value)
encode_json()
sql.postgres.types.encode_json(value)
encode_jsonb()
sql.postgres.types.encode_jsonb(value)
jsonb takes the same text behind the version byte the server expects.
encode_uuid()
sql.postgres.types.encode_uuid(value)
encode_date()
sql.postgres.types.encode_date(value)
encode_timestamp()
sql.postgres.types.encode_timestamp(value)
encode_array()
sql.postgres.types.encode_array(value, element_oid, registry)
Builds the binary form of an array.
One dimension only. PostgreSQL’s arrays can nest, but a nested Zuri list would have to be rectangular to become one, and quietly refusing a ragged list is worse than not offering it.
decode_array()
sql.postgres.types.decode_array(buffer, oid, registry)
Reads an array back, however many dimensions it has.
Classes
TypeRegistry
class sql.postgres.TypeRegistry
Which decoder and encoder each OID uses.
A registry can be extended, which is how a program teaches the adapter about a type its database defines that PostgreSQL does not ship:
registry.register(oid, decoder, encoder)
The important question a registry answers is knows(): the adapter asks
for a column in binary only when the answer is yes, and asks for text
otherwise, so an unregistered type arrives as a readable string rather
than as bytes.
Constructor
sql.postgres.TypeRegistry()
TypeRegistry.register()
sql.postgres.TypeRegistry.register(oid: number, decoder, encoder)
Adds or replaces the codec for one OID.
Parameters
oid(number)decoder(function) — Takes(bytes, oid)and returns a value.encoder(function) — Takes a value and returns a list of bytes.
TypeRegistry.knows()
sql.postgres.TypeRegistry.knows(oid: number) -> bool
Whether this registry can read oid in binary.
Parameters
oid(number)
Returns bool
TypeRegistry.can_encode()
sql.postgres.TypeRegistry.can_encode(oid: number) -> bool
Whether this registry can write oid in binary.
A parameter whose type has no encoder is sent as text and left to the server to read, which it can do for every type it knows.
Parameters
oid(number)
Returns bool
TypeRegistry.decode()
sql.postgres.TypeRegistry.decode(buffer, oid: number) -> any
Reads a value from its binary form.
Parameters
buffer(bytes)oid(number)
Returns any
TypeRegistry.encode()
sql.postgres.TypeRegistry.encode(value, oid: number) -> list[number]
Writes a value in binary form.
Parameters
value(any)oid(number)
Returns list[number]
TypeRegistry.infer()
sql.postgres.TypeRegistry.infer(value) -> number
The OID that best carries value when the server has not said which
type it expects.
Parameters
value(any)
Returns number
2026, Richard Ore and Zuri contributors
sql.result
import sql.result
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.result.*needsimport sql.result.
What a statement hands back: the rows of a query, or the count of a statement that changed something.
Rows are dictionaries keyed by column name, which is what makes
row.title read the way it does. Where that is not enough, because a
join selected two columns of the same name or because position is what
matters, columns and tuples() give the ordered view of the same
result.
Classes
ResultSet
class sql.ResultSet
The rows a query returned, with the shape of the result beside them.
Iterating a result set walks its rows, so the common case needs nothing else:
for row in db.query('select id, title from posts') {
echo '${row.id}: ${row.title}'
}
- printable — has a
@to_string(), soechoandprint()show something useful - iterable — can be walked with
foranditer
Fields
| Field | Type | Description |
|---|---|---|
columns | list[dict] | The result’s columns in order, each a dictionary with name and type. |
rows | list[dict] | The rows, each a dictionary keyed by column name. |
command | The engine’s description of what ran, such as 'SELECT 3', or nil for an engine that reports nothing. |
Constructor
sql.ResultSet(columns, rows, command)
Parameters
columns(list) — Column descriptors.rows(list) — Rows as lists of values, in column order.command(string|nil) — The engine’s command tag.
ResultSet.length()
sql.ResultSet.length() -> number
How many rows the result holds.
Returns number
ResultSet.is_empty()
sql.ResultSet.is_empty() -> bool
Whether the result holds no rows at all.
Returns bool
ResultSet.first()
sql.ResultSet.first() -> dict|nil
The first row, or nil for an empty result.
Returns dict|nil
ResultSet.last()
sql.ResultSet.last() -> dict|nil
The last row, or nil for an empty result.
Returns dict|nil
ResultSet.column_names()
sql.ResultSet.column_names() -> list[string]
The names of the result’s columns, in order.
Returns list[string]
ResultSet.column()
sql.ResultSet.column(column) -> list
Every value of one column, in row order.
Parameters
column(string|number) — A column name, or its position.
Returns list
Raises QueryError if the result has no such column.
ResultSet.scalar()
sql.ResultSet.scalar(fallback) -> any
The first column of the first row, for a query written to return exactly one value.
var total = db.query('select count(*) from posts').scalar()
Parameters
fallback(any) — What to return for an empty result.
Returns any
ResultSet.tuples()
sql.ResultSet.tuples() -> list[list]
The rows as lists of values in column order, rather than as dictionaries.
This is the view that survives duplicate column names.
Returns list[list]
ResultSet.to_string()
sql.ResultSet.to_string()
ExecResult
class sql.ExecResult
What a statement that changed something reports.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
rows_affected | number | How many rows the statement inserted, updated or deleted. |
last_insert_id | The id of the row just inserted, where the engine reports one. |
Constructor
sql.ExecResult(rows_affected, last_insert_id)
Parameters
rows_affected(number)last_insert_id(number|bigint|nil)
ExecResult.to_string()
sql.ExecResult.to_string()
2026, Richard Ore and Zuri contributors
sql.schema
import sql.schema
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.schema.*needsimport sql.schema.
Asking a database what is in it.
Every engine can list its tables and describe its columns, and every
engine does it differently: PostgreSQL has information_schema, SQLite
has PRAGMA table_info. The queries have nothing in common, so each
adapter writes its own and they all return the same shape.
for table in db.schema.tables() {
echo table
}
for column in db.schema.columns('posts') {
echo '${column.name} ${column.type}${column.nullable ? "" : " not null"}'
}
This is what makes insert() portable: on an engine with no last insert
id, the insert needs a RETURNING clause, and the column to return is
the table’s primary key, which is asked for here.
Functions
column_shape()
sql.column_shape() -> dict
The description of one column, as every adapter returns it.
| Key | Meaning |
|---|---|
name | The column’s name. |
type | The engine’s own name for its type. |
nullable | Whether it accepts NULL. |
default_value | Its default as SQL text, or nil for none. |
primary_key | Whether it is part of the primary key. |
position | Its position in the table, counting from one. |
Returns dict
Classes
Schema
class sql.Schema
Introspection for one connection.
The methods here delegate to the adapter, which is where the queries
that differ per engine live. An adapter that has not implemented
introspection raises NotSupportedError rather than returning something
empty, so “this engine cannot tell you” never looks like “there is
nothing there”.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.Schema(connection)
Parameters
connection(Connection)
Schema.tables()
sql.Schema.tables(schema) -> list[string]
The names of the tables in the database.
Tables the engine keeps for itself are left out, so what comes back is what the application created.
Parameters
schema(string|nil) — Which schema to look in, on an engine that has them. The default schema when nil.
Returns list[string]
Schema.views()
sql.Schema.views(schema) -> list[string]
The names of the views in the database.
Parameters
schema(string|nil)
Returns list[string]
Schema.has_table()
sql.Schema.has_table(table: string, schema) -> bool
Whether table exists.
Parameters
table(string)schema(string|nil)
Returns bool
Schema.columns()
sql.Schema.columns(table: string, schema) -> list[dict]
The columns of table, in order.
Parameters
table(string)schema(string|nil)
Returns list[dict] — Each as described by column_shape().
Schema.column_names()
sql.Schema.column_names(table: string, schema) -> list[string]
The names of table’s columns, in order.
Parameters
table(string)schema(string|nil)
Returns list[string]
Schema.has_column()
sql.Schema.has_column(table: string, column: string, schema) -> bool
Whether table has a column called column.
Parameters
table(string)column(string)schema(string|nil)
Returns bool
Schema.primary_key()
sql.Schema.primary_key(table: string, schema) -> string|nil
The name of table’s primary key column, or nil.
Nil for a table with no primary key and for one whose primary key spans
several columns, since neither has a single column to name.
primary_key_columns() covers the composite case.
Parameters
table(string)schema(string|nil)
Returns string|nil
Schema.primary_key_columns()
sql.Schema.primary_key_columns(table: string, schema) -> list[string]
The columns making up table’s primary key, in key order.
Parameters
table(string)schema(string|nil)
Returns list[string]
Schema.indexes()
sql.Schema.indexes(table: string, schema) -> list[dict]
The indexes on table.
Each is { name, columns, unique }.
Parameters
table(string)schema(string|nil)
Returns list[dict]
Schema.foreign_keys()
sql.Schema.foreign_keys(table: string, schema) -> list[dict]
The foreign keys declared on table.
Each is { columns, references_table, references_columns, on_delete, on_update }.
Parameters
table(string)schema(string|nil)
Returns list[dict]
Schema.to_string()
sql.Schema.to_string()
2026, Richard Ore and Zuri contributors
sql.sqlite
import sql.sqlite
sqldoes not re-export this module, so it is reached only by importing it directly.
The SQLite adapter.
SQLite is a database that is a file. There is no server to run, no port to open and no user to create, which is what makes it the right choice for an application that ships with its data, a test suite that wants a real database per run, and anything that would otherwise have invented a file format of its own.
Reach it through sql, which is what makes the same program work
against another engine later:
import sql
var db = sql.open('sqlite://./app.db')
The module underneath is here for the parts of SQLite that have no equivalent anywhere else: incremental blob access, online backup, functions and collations written in Zuri, and the hooks that report what the engine is doing.
Submodules
| Module | Reached as | Summary |
|---|---|---|
sql.sqlite.backup | sql.sqlite.backup.* | Copying a live SQLite database without stopping it. |
sql.sqlite.blob | sql.sqlite.blob.* | Reading and writing a single blob-valued cell a piece at a time. |
sql.sqlite.connection | sql.sqlite.connection.* | A SQLite connection, as the layer above the adapters sees one. |
sql.sqlite.cursor | sql.sqlite.cursor.* | Reading a SQLite result one row at a time. |
sql.sqlite.driver | sql.sqlite.driver.* | Opening SQLite databases: reading a connection string, choosing the open flags, and putting a new connection… |
sql.sqlite.errors | sql.sqlite.errors.* | Turning SQLite’s result codes into the shared error classes. |
sql.sqlite.schema | import sql.sqlite.schema | Introspection for SQLite, which answers through pragmas rather than through catalogue tables. |
sql.sqlite.statement | sql.sqlite.statement.* | A SQLite statement compiled once and run many times. |
sql.sqlite.types | sql.sqlite.types.* | Reading SQLite values back as the types a column was declared to hold. |
Constants
DRIVER
sql.sqlite.DRIVER
The driver instance sql registers for this engine.
A driver holds no state of its own, so one serves every connection.
Functions
connect()
sql.sqlite.connect(dsn) -> SqliteConnection
Opens a SQLite database directly, without going through sql.
Useful when a program has settled on SQLite and wants the adapter’s own
extras without the indirection. Anything meant to stay portable should
use sql.open() instead.
Parameters
dsn(string|dict) — A path, asqlite:string, or options.
Returns SqliteConnection
2026, Richard Ore and Zuri contributors
sql.sqlite.backup
import sql.sqlite.backup
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.sqlite.backup.*needsimport sql.sqlite.backup.
Copying a live SQLite database without stopping it.
Copying the file with the filesystem is only safe when nothing is writing, which for a running application is never guaranteed. SQLite’s backup interface copies page by page and notices when the source changes underneath, restarting the copy rather than producing a file that is half of one state and half of another.
db.native().backup_to('./snapshot.db')
That copies everything in one call. For a large database where the pause
matters, Backup copies in steps so other work can happen in between.
Constants
DEFAULT_PAGES
sql.sqlite.backup.DEFAULT_PAGES = 256
How many pages a step copies when no size is given.
Classes
Backup
class sql.sqlite.Backup
A copy in progress between two connections.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.sqlite.Backup(handle)
Parameters
ptr— handle The native backup handle.
Backup.step()
sql.sqlite.Backup.step(pages) -> string
Copies up to pages pages.
Parameters
pages(number|nil) — How many, or nil forDEFAULT_PAGES. A negative number copies everything remaining.
Returns string — 'done' when the copy is complete, 'ok' when
more remains, and 'busy' or 'locked' when the source moved and the
step should be tried again.
Backup.remaining()
sql.sqlite.Backup.remaining() -> number
Pages still to copy, as of the last step.
Returns number
Backup.total()
sql.sqlite.Backup.total() -> number
Pages in the source, as of the last step.
Returns number
Backup.progress()
sql.sqlite.Backup.progress() -> number
How much of the copy is done, from 0 to 1.
Returns number
Backup.run()
sql.sqlite.Backup.run(pages, attempts)
Runs the copy to completion, retrying the steps the source interrupts.
Parameters
pages(number|nil) — How many pages per step.attempts(number|nil) — How many times to retry a busy step before giving up. 100 when nil.
Raises SqlError if the source stays busy for that many attempts.
Backup.close()
sql.sqlite.Backup.close()
Ends the copy. Safe to call more than once, and legitimate on an unfinished copy, which abandons it and leaves the destination as it was.
Backup.is_closed()
sql.sqlite.Backup.is_closed() -> bool
Whether this backup has finished or been abandoned.
Returns bool
Backup.to_string()
sql.sqlite.Backup.to_string()
2026, Richard Ore and Zuri contributors
sql.sqlite.blob
import sql.sqlite.blob
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.sqlite.blob.*needsimport sql.sqlite.blob.
Reading and writing a single blob-valued cell a piece at a time.
A large value stored in a database is usually read with a select,
which means the whole of it arrives at once. For a hundred megabyte file
that is a hundred megabytes of memory to move a copy from one place to
another. A blob handle opens one cell and reads or writes windows of it,
so the memory needed is the size of the window.
var blob = db.native().blob('main', 'files', 'content', id, false)
var at = 0
while at < blob.length() {
var chunk = blob.read(at, 65536.min(blob.length() - at))
out.write(chunk)
at += chunk.length()
}
blob.close()
The cell has to exist and already be the right size: SQLite cannot grow
a blob through this interface. The usual approach is to insert
zeroblob(n) for the final size and then write into it.
Classes
Blob
class sql.sqlite.Blob
An open blob handle.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.sqlite.Blob(handle, writable)
Parameters
ptr— handle The native blob handle.writable(bool) — Whether it was opened for writing.
Blob.length()
sql.sqlite.Blob.length() -> number
How many bytes the blob holds. Fixed for the handle’s lifetime.
Returns number
Blob.is_writable()
sql.sqlite.Blob.is_writable() -> bool
Whether this handle may write.
Returns bool
Blob.read()
sql.sqlite.Blob.read(offset: number, length: number) -> bytes
Reads length bytes starting at offset.
Parameters
offset(number)length(number)
Returns bytes
Raises SqlError if the window runs past the end of the blob.
Blob.read_all()
sql.sqlite.Blob.read_all() -> bytes
Reads the whole blob. Defeats the point of a blob handle, and is here for the case where one turns out to be small.
Returns bytes
Blob.write()
sql.sqlite.Blob.write(data, offset: number)
Writes data starting at offset.
Parameters
data(bytes)offset(number)
Raises SqlError if the handle is read-only or the write would run
past the end.
Blob.reopen()
sql.sqlite.Blob.reopen(rowid: number)
Points this handle at the same column of a different row.
Cheaper than closing and opening again, which is why it exists.
Parameters
rowid(number)
Blob.close()
sql.sqlite.Blob.close()
Releases the handle. Safe to call more than once.
Blob.is_closed()
sql.sqlite.Blob.is_closed() -> bool
Whether this handle has been closed.
Returns bool
Blob.to_string()
sql.sqlite.Blob.to_string()
2026, Richard Ore and Zuri contributors
sql.sqlite.connection
import sql.sqlite.connection
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.sqlite.connection.*needsimport sql.sqlite.connection.
A SQLite connection, as the layer above the adapters sees one.
SQLite has no server, so a connection here is an open database file (or an in-memory database) and the handle onto it. That difference shows through in a few places, and this is where each of them is dealt with.
Transactions
SQLite has one transaction per connection and no nesting, so nested
transactions are savepoints, as they are for every other adapter. Its
isolation levels are not levels but locking modes: BEGIN DEFERRED
takes no lock until the first statement, BEGIN IMMEDIATE takes the
write lock at once. Serializable maps onto IMMEDIATE, which is what
actually delivers it, and the levels SQLite cannot approximate raise
rather than being quietly accepted.
One writer
A SQLite database takes one writer at a time. Where another engine would
queue, SQLite returns SQLITE_BUSY, which this adapter turns into a
TimeoutError once the busy timeout expires. Setting a busy timeout is
what turns a burst of contention into a wait rather than an error, so
connections open with one already set.
Constants
CACHE_LIMIT
sql.sqlite.connection.CACHE_LIMIT = 64
How many compiled statements a connection keeps for reuse.
Statements run through execute() and select() are cached by their
text, since those compile, run and reset within the one call and are
never handed out. A cursor’s statement is never cached: it stays live
between calls, and lending the same handle to two readers would have
them stepping each other’s result.
Classes
SqliteConnection
class sql.sqlite.SqliteConnection < DriverConnection
An open SQLite database.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.sqlite.SqliteConnection(driver, handle, options)
Parameters
driver(Driver) — The driver that opened this.ptr— handle The native connection.options(dict) — The options it was opened with.
SqliteConnection.handle()
sql.sqlite.SqliteConnection.handle()
The native handle, for the parts of SQLite that are not part of the shared contract: blobs, backups, user-defined functions and hooks.
Returns — ptr
SqliteConnection.schema_adapter()
sql.sqlite.SqliteConnection.schema_adapter() -> SqliteSchema
This connection’s introspection, built once and reused.
Returns SqliteSchema
SqliteConnection.execute()
sql.sqlite.SqliteConnection.execute(sql: string, values: list) -> dict
Runs a statement for its effect.
Parameters
sql(string)values(list)
Returns dict — { rows_affected, last_insert_id }
SqliteConnection.select()
sql.sqlite.SqliteConnection.select(sql: string, values: list) -> dict
Runs a statement and reads its whole result.
Parameters
sql(string)values(list)
Returns dict — { columns, rows }
SqliteConnection.open_cursor()
sql.sqlite.SqliteConnection.open_cursor(sql: string, values: list, options: dict) -> SqliteCursor
Runs a statement and hands back a cursor over its result.
The statement is compiled fresh rather than taken from the cache, because the cursor keeps it for as long as it is reading.
Parameters
sql(string)values(list)options(dict) — Unused; SQLite produces rows on demand already, so there is no batch size to choose.
Returns SqliteCursor
SqliteConnection.prepare()
sql.sqlite.SqliteConnection.prepare(sql: string) -> SqliteStatement
Compiles a statement for repeated use.
Parameters
sql(string)
Returns SqliteStatement
SqliteConnection.begin()
sql.sqlite.SqliteConnection.begin(isolation)
Opens a transaction, or takes a savepoint when one is already open.
Parameters
isolation(string|nil)
Raises NotSupportedError for a level SQLite has no analogue for.
SqliteConnection.commit()
sql.sqlite.SqliteConnection.commit()
Commits the transaction, or releases the innermost savepoint.
SqliteConnection.rollback()
sql.sqlite.SqliteConnection.rollback()
Rolls the transaction back, or undoes the innermost savepoint.
SqliteConnection.savepoint()
sql.sqlite.SqliteConnection.savepoint(name: string)
SqliteConnection.release_savepoint()
sql.sqlite.SqliteConnection.release_savepoint(name: string)
SqliteConnection.rollback_to()
sql.sqlite.SqliteConnection.rollback_to(name: string)
SqliteConnection.in_transaction()
sql.sqlite.SqliteConnection.in_transaction() -> bool
Whether a transaction is open.
Returns bool
SqliteConnection.depth()
sql.sqlite.SqliteConnection.depth() -> number
How deeply transactions are currently nested: zero outside one, one inside the outermost, and one more per savepoint.
Returns number
SqliteConnection.changes()
sql.sqlite.SqliteConnection.changes() -> number
How many rows the last statement changed.
Returns number
SqliteConnection.last_insert_id()
sql.sqlite.SqliteConnection.last_insert_id() -> number|bigint|nil
The row id of the last insert on this connection, or nil if there has not been one.
Returns number|bigint|nil
SqliteConnection.ping()
sql.sqlite.SqliteConnection.ping() -> bool
Checks the connection is usable.
Returns bool
SqliteConnection.server_version()
sql.sqlite.SqliteConnection.server_version() -> string
SQLite’s version, such as '3.53.2'.
Returns string
SqliteConnection.interrupt()
sql.sqlite.SqliteConnection.interrupt()
Asks a running statement on this connection to stop.
The statement fails rather than being killed, and the connection stays usable. Meant for a long query on a connection another isolate is waiting on.
SqliteConnection.set_busy_timeout()
sql.sqlite.SqliteConnection.set_busy_timeout(milliseconds: number)
How long a statement waits for another writer before giving up.
Parameters
milliseconds(number) — Zero waits not at all.
SqliteConnection.blob()
sql.sqlite.SqliteConnection.blob(database: string, table: string, column: string, rowid: number, writable) -> Blob
Opens one blob-valued cell for piecewise reading and writing.
Parameters
database(string) — The attached database, usually'main'.table(string)column(string)rowid(number)writable(bool|nil) — Read-only when nil or false.
Returns Blob
SqliteConnection.backup()
sql.sqlite.SqliteConnection.backup(destination, options) -> Backup
Starts a copy of this database into destination.
Parameters
destination(SqliteConnection) — An open connection to copy into, which must not be this one.options(dict|nil) —sourceandtargetname which attached database on each side; both default to'main'.
Returns Backup
SqliteConnection.backup_to()
sql.sqlite.SqliteConnection.backup_to(path: string, options)
Copies this database to a file, all in one call.
The file is created if it is not there. An existing one is copied over rather than appended to, so a snapshot taken repeatedly to the same path replaces itself.
db.native().backup_to('./snapshot.db', nil)
For a copy that is not a file, or one taken in steps, open the
destination yourself and use backup().
Parameters
path(string)options(dict|nil) — Asbackup()takes.
SqliteConnection.create_function()
sql.sqlite.SqliteConnection.create_function(name: string, arity: number, body: function, options)
Adds a function to this connection, written in Zuri.
db.native().create_function('initials', 1, @(name) {
return ''.join(name.split(' ').map(@(part) => part[0, 1]))
})
db.query('select initials(name) from authors')
The function runs inside the engine, once per row, so it can be used anywhere an expression can. It must not touch the connection it was registered on; SQLite is in the middle of a statement when it calls, and reentering would deadlock.
Parameters
name(string)arity(number) — How many arguments, or -1 for any number.body(function)options(dict|nil) —deterministicsays the result depends only on the arguments, which lets SQLite use the function in an index and hoist it out of loops. True unless said otherwise.
SqliteConnection.create_aggregate()
sql.sqlite.SqliteConnection.create_aggregate(name: string, arity: number, step: function, finish: function)
Adds an aggregate function, written as a fold.
step is called once per row with the accumulator so far followed by
the row’s arguments, and returns the next accumulator. finish is
called once per group with the last accumulator and returns the group’s
value. The accumulator starts as nil, which is also what finish sees
for a group with no rows in it.
db.native().create_aggregate('longest', 1,
@(longest, word) {
if longest == nil or word.length() > longest.length() {
return word
}
return longest
},
@(longest) => longest
)
Parameters
name(string)arity(number)step(function)finish(function)
SqliteConnection.create_collation()
sql.sqlite.SqliteConnection.create_collation(name: string, compare: function)
Adds a collation, which is an ordering for text.
The callable is passed two strings and returns a negative number, zero
or a positive number, the same shape a sort comparator takes. A
statement reaches it with order by column collate name.
Parameters
name(string)compare(function)
SqliteConnection.delete_function()
sql.sqlite.SqliteConnection.delete_function(name: string, arity: number)
Removes a function or aggregate registered under this name and arity.
Parameters
name(string)arity(number)
SqliteConnection.on_change()
sql.sqlite.SqliteConnection.on_change(handler)
Called after each row an INSERT, UPDATE or DELETE changes, with the operation name, the database, the table and the row id.
Rows a trigger or a foreign key action changes are reported too. Each change is reported as it is made, so one a transaction later rolls back has still been reported. A DELETE with no WHERE clause empties the table without visiting its rows, and a WITHOUT ROWID table has no rowid to report, so neither is reported at all.
Parameters
handler(function|nil) — Nil clears it.
SqliteConnection.on_commit()
sql.sqlite.SqliteConnection.on_commit(handler)
Called just before each commit. Returning false turns the commit into
a rollback.
Parameters
handler(function|nil) — Nil clears it.
SqliteConnection.on_rollback()
sql.sqlite.SqliteConnection.on_rollback(handler)
Called whenever a transaction rolls back, however it was caused.
Parameters
handler(function|nil) — Nil clears it.
SqliteConnection.set_authorizer()
sql.sqlite.SqliteConnection.set_authorizer(handler)
Consulted for every action a statement wants to take, as the statement is compiled rather than as it runs.
The callable is passed the action code and the four strings SQLite
supplies about it, and answers 'allow', 'deny' or 'ignore'.
Anything else counts as denial, so a handler that falls off the end
fails closed.
Parameters
handler(function|nil) — Nil clears it.
SqliteConnection.on_progress()
sql.sqlite.SqliteConnection.on_progress(instructions: number, handler)
Called every instructions steps of a long statement. Returning false
interrupts it.
Parameters
instructions(number)handler(function|nil) — Nil clears it.
SqliteConnection.close()
sql.sqlite.SqliteConnection.close()
Closes the connection, releasing every statement it cached.
Safe to call more than once.
SqliteConnection.is_closed()
sql.sqlite.SqliteConnection.is_closed()
SqliteConnection.to_string()
sql.sqlite.SqliteConnection.to_string()
2026, Richard Ore and Zuri contributors
sql.sqlite.cursor
import sql.sqlite.cursor
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.sqlite.cursor.*needsimport sql.sqlite.cursor.
Reading a SQLite result one row at a time.
SQLite computes rows as they are asked for rather than building the whole result first, so a cursor here is the engine’s own behaviour exposed rather than a batching scheme layered on top. A query over a large table holds one row at a time however long the table is.
The statement a cursor is stepping belongs to it until it closes. That is why the connection compiles a separate statement for each cursor rather than lending one from its cache.
Classes
SqliteCursor
class sql.sqlite.SqliteCursor < DriverCursor
A result being stepped.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.sqlite.SqliteCursor(statement, columns, conversions, query, owned)
Parameters
ptr— statement A prepared native statement, already bound.columns(list) — Column descriptors.conversions(list) — Per column conversions.query(string) — The statement, for error messages.owned(bool) — Whether closing the cursor finalizes the statement, which it does for a cursor the connection compiled and does not for one belonging to a prepared statement the caller still holds.
SqliteCursor.columns()
sql.sqlite.SqliteCursor.columns()
SqliteCursor.next_row()
sql.sqlite.SqliteCursor.next_row() -> list|nil
The next row, or nil once the result is finished.
Reaching the end closes the cursor, so a loop that runs to exhaustion releases the statement without having to remember to.
Returns list|nil
SqliteCursor.close()
sql.sqlite.SqliteCursor.close()
Releases the cursor. Safe to call more than once, and safe to call on a cursor that already ran to the end.
SqliteCursor.is_closed()
sql.sqlite.SqliteCursor.is_closed() -> bool
Whether this cursor has been closed or run to exhaustion.
Returns bool
SqliteCursor.to_string()
sql.sqlite.SqliteCursor.to_string()
2026, Richard Ore and Zuri contributors
sql.sqlite.driver
import sql.sqlite.driver
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.sqlite.driver.*needsimport sql.sqlite.driver.
Opening SQLite databases: reading a connection string, choosing the open flags, and putting a new connection into a sane state.
Constants
MEMORY
sql.sqlite.MEMORY = ':memory:'
The path that means a private database held in memory, which is discarded when the connection closes.
OPEN_READONLY
sql.sqlite.driver.OPEN_READONLY = 1
OPEN_READWRITE
sql.sqlite.driver.OPEN_READWRITE = 2
OPEN_CREATE
sql.sqlite.driver.OPEN_CREATE = 4
OPEN_URI
sql.sqlite.driver.OPEN_URI = 64
OPEN_MEMORY
sql.sqlite.driver.OPEN_MEMORY = 128
OPEN_NOMUTEX
sql.sqlite.driver.OPEN_NOMUTEX = 32768
OPEN_FULLMUTEX
sql.sqlite.driver.OPEN_FULLMUTEX = 65536
OPEN_SHAREDCACHE
sql.sqlite.driver.OPEN_SHAREDCACHE = 131072
OPEN_PRIVATECACHE
sql.sqlite.driver.OPEN_PRIVATECACHE = 262144
DEFAULT_BUSY_TIMEOUT
sql.sqlite.DEFAULT_BUSY_TIMEOUT = 5000
How long a statement waits for another writer by default.
SQLite’s own default is not to wait at all, which turns any contention into an immediate error. Five seconds is long enough to ride out the short bursts that make up most contention and short enough that a genuine deadlock still surfaces.
MAX_PARAMETERS
sql.sqlite.MAX_PARAMETERS = 32766
Most parameters one statement can bind, which is SQLite’s own limit.
Classes
SqliteDriver
class sql.sqlite.SqliteDriver < Driver
Opens SQLite databases.
SqliteDriver.name()
sql.sqlite.SqliteDriver.name()
SqliteDriver.schemes()
sql.sqlite.SqliteDriver.schemes()
SqliteDriver.capabilities()
sql.sqlite.SqliteDriver.capabilities() -> dict
What SQLite can do.
Two of these are worth reading twice. concurrent_writers is false: a
SQLite database takes one writer at a time whatever the journal mode,
and a pool over it serialises writes rather than parallelising them.
json is false because SQLite has JSON functions but no JSON type,
so a list or dictionary is stored as text and comes back as text unless
the column says otherwise.
Returns dict
SqliteDriver.parse_dsn()
sql.sqlite.SqliteDriver.parse_dsn(dsn) -> dict
Reads a connection string, or passes an options dictionary through with its defaults filled in.
These all name the same database:
./app.db
sqlite:app.db
sqlite://./app.db
sqlite:///absolute/path/app.db
and these all mean a private in-memory database:
:memory:
sqlite::memory:
sqlite://:memory:
A file: URI is handed to SQLite’s own URI handling, which is how the
options SQLite spells itself are reached:
file:app.db?mode=ro&cache=shared
Anything after ? on a non-URI form is read as this adapter’s own
options instead. The recognised keys are mode (ro, rw, rwc or
memory), busy_timeout in milliseconds, foreign_keys,
journal_mode and cache (shared or private).
Parameters
dsn(string|dict)
Returns dict
Raises ConnectionError if the string names no database.
SqliteDriver.connect()
sql.sqlite.SqliteDriver.connect(options: dict) -> SqliteConnection
Opens the database and puts it into the state the options ask for.
Parameters
options(dict) — Fromparse_dsn().
Returns SqliteConnection
2026, Richard Ore and Zuri contributors
sql.sqlite.errors
import sql.sqlite.errors
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.sqlite.errors.*needsimport sql.sqlite.errors.
Turning SQLite’s result codes into the shared error classes.
SQLite reports failures as small integers. The native layer puts the
extended code in front of every message it raises, in the form [2067] UNIQUE constraint failed: t.a,
and this module reads it back out and picks the class that says the same
thing in the vocabulary every adapter shares.
Extended codes rather than primary ones, because the distinctions worth acting on live there: every constraint failure is the primary code 19, and only the extended code says whether it was a unique index, a foreign key or a NOT NULL column.
Functions
primary()
sql.sqlite.errors.primary(code: number) -> number
An extended code’s primary code, which is its low byte.
Parameters
code(number)
Returns number
class_for()
sql.sqlite.class_for(code: number) -> class
The class that best describes code.
Parameters
code(number) — An extended result code.
Returns class
split_message()
sql.sqlite.split_message(message: string) -> dict
Splits the [code] message form the native layer raises.
A message without the prefix comes back with a nil code and its text unchanged, so an error raised anywhere other than the native layer still passes through this cleanly.
Parameters
message(string)
Returns dict — { code, text }
reraise()
sql.sqlite.reraise(error, query)
Re-raises an error from the native layer as the right class.
Anything that is already a SqlError passes through untouched, so
wrapping a call that itself went through here does not bury the specific
class under a general one.
Parameters
error(Error) — The error the native layer raised.query(string|nil) — The statement that was running, for context.
Raises SqlError always.
2026, Richard Ore and Zuri contributors
sql.sqlite.schema
import sql.sqlite.schema
sqldoes not re-export this module, so it is reached only by importing it directly.
Introspection for SQLite, which answers through pragmas rather than through catalogue tables.
PRAGMA table_info, index_list, index_info and foreign_key_list
are functions that return result sets, so each of these is an ordinary
query. The one catalogue table SQLite does have, sqlite_master, is
where the list of tables comes from.
SQLite has no schemas. It has attached databases, which the schema
argument here names: nil and 'main' both mean the database the
connection opened, and 'temp' holds temporary tables.
Classes
SqliteSchema
class sql.sqlite.schema.SqliteSchema < SchemaAdapter
Answers introspection questions about a SQLite database.
Constructor
sql.sqlite.schema.SqliteSchema(connection)
Parameters
connection(SqliteConnection)
SqliteSchema.tables()
sql.sqlite.schema.SqliteSchema.tables(schema) -> list[string]
The tables the application created.
SQLite keeps its own bookkeeping in tables whose names begin with
sqlite_, so those are left out.
Parameters
schema(string|nil) — An attached database name.
Returns list[string]
SqliteSchema.views()
sql.sqlite.schema.SqliteSchema.views(schema) -> list[string]
The views.
Parameters
schema(string|nil)
Returns list[string]
SqliteSchema.columns()
sql.sqlite.schema.SqliteSchema.columns(table: string, schema) -> list[dict]
The columns of table, in declaration order.
Parameters
table(string)schema(string|nil)
Returns list[dict]
SqliteSchema.primary_key_columns()
sql.sqlite.schema.SqliteSchema.primary_key_columns(table: string, schema) -> list[string]
The primary key columns, in key order.
table_info reports each key column’s position within the key, so a
composite key comes back in the order it was declared rather than in
column order.
Parameters
table(string)schema(string|nil)
Returns list[string]
SqliteSchema.indexes()
sql.sqlite.schema.SqliteSchema.indexes(table: string, schema) -> list[dict]
The indexes on table.
The implicit index behind a column’s UNIQUE or PRIMARY KEY
declaration is included, as SQLite reports it, since it is a real index
and constrains the table the same way a named one does.
Parameters
table(string)schema(string|nil)
Returns list[dict]
SqliteSchema.foreign_keys()
sql.sqlite.schema.SqliteSchema.foreign_keys(table: string, schema) -> list[dict]
The foreign keys declared on table.
A key spanning several columns is reported by SQLite as one row per column sharing an id, so the rows are gathered back into one entry per key.
Parameters
table(string)schema(string|nil)
Returns list[dict]
2026, Richard Ore and Zuri contributors
sql.sqlite.statement
import sql.sqlite.statement
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.sqlite.statement.*needsimport sql.sqlite.statement.
A SQLite statement compiled once and run many times.
Compiling a statement is the expensive half of running one, and a statement run in a loop pays that cost once here rather than on every pass. The saving is real enough to be worth the extra object: an insert of ten thousand rows through a prepared statement does one compile instead of ten thousand.
A statement holds one native handle, so it can only be doing one thing at a time. Opening a cursor over it takes that handle until the cursor closes, and running it again in the meantime raises rather than silently resetting the cursor underneath.
Functions
describe()
sql.sqlite.statement.describe(statement) -> dict
Describes the columns a compiled statement returns.
Parameters
ptr— statement
Returns dict — { columns, conversions }
bind_all()
sql.sqlite.statement.bind_all(statement, values: list, query: string)
Binds values to a reset statement, in order.
Parameters
ptr— statementvalues(list)query(string) — For error messages.
Raises QueryError if the count does not match what the statement
expects.
Classes
SqliteStatement
class sql.sqlite.SqliteStatement < DriverStatement
A prepared SQLite statement.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.sqlite.SqliteStatement(connection, statement, query)
Parameters
connection(SqliteConnection) — The connection that owns this.ptr— statement The compiled native statement.query(string) — The statement text, as sent to the engine.
SqliteStatement.columns()
sql.sqlite.SqliteStatement.columns()
SqliteStatement.parameter_count()
sql.sqlite.SqliteStatement.parameter_count()
SqliteStatement.execute()
sql.sqlite.SqliteStatement.execute(values: list) -> dict
Runs the statement for its effect.
Parameters
values(list)
Returns dict — { rows_affected, last_insert_id }
SqliteStatement.select()
sql.sqlite.SqliteStatement.select(values: list) -> dict
Runs the statement and reads its whole result.
Parameters
values(list)
Returns dict — { columns, rows }
SqliteStatement.open_cursor()
sql.sqlite.SqliteStatement.open_cursor(values: list, options: dict) -> SqliteCursor
Runs the statement and hands back a cursor over its result.
The cursor holds this statement until it closes.
Parameters
values(list)options(dict) — Unused here; SQLite streams natively.
Returns SqliteCursor
SqliteStatement.close()
sql.sqlite.SqliteStatement.close()
Releases the statement. Safe to call more than once.
SqliteStatement.is_closed()
sql.sqlite.SqliteStatement.is_closed() -> bool
Whether this statement has been closed.
Returns bool
SqliteStatement.to_string()
sql.sqlite.SqliteStatement.to_string()
2026, Richard Ore and Zuri contributors
sql.sqlite.types
import sql.sqlite.types
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.sqlite.types.*needsimport sql.sqlite.types.
Reading SQLite values back as the types a column was declared to hold.
SQLite stores five things and remembers nothing about intent. A boolean goes in as 1 and comes back as the number 1; a timestamp goes in as text and comes back as text. What it does keep is the type each column was declared with, and that is enough to put the value back the way it went in.
So decoding here is driven by the declaration and never by the value. A
column declared BOOLEAN yields true and false; one declared
INTEGER yields the number 1 even when every row in it happens to be 0
or 1. Guessing from content would make the type of a result depend on
the rows it happened to return, which is the kind of thing that works
until the day a table has different data in it.
A column with no declared type, which is what an expression has, comes back exactly as SQLite stored it.
Constants
BOOLEAN_TYPES
sql.sqlite.types.BOOLEAN_TYPES = [...]
Declared types that mean a boolean.
TEMPORAL_TYPES
sql.sqlite.types.TEMPORAL_TYPES = [...]
Declared types that mean a point in time.
JSON_TYPES
sql.sqlite.types.JSON_TYPES = [...]
Declared types that mean JSON held in a text column.
Functions
base_type()
sql.sqlite.base_type(declared) -> string
The bare type name from a declaration, upper cased and without any size or precision.
VARCHAR(255) is VARCHAR, decimal(10, 2) is DECIMAL, and an
undeclared column is the empty string.
Parameters
declared(string|nil)
Returns string
conversion_for()
sql.sqlite.conversion_for(declared) -> string|nil
Which conversion a column’s declaration calls for: 'bool', 'date',
'json', or nil for a column that needs none.
Parameters
declared(string|nil) — The column’s declared type.
Returns string|nil
decode()
sql.sqlite.types.decode(value, conversion) -> any
Applies one column’s conversion to one stored value.
NULL stays nil whatever the column was declared as. A value that will
not convert, such as text in a DATETIME column that is not a
timestamp, comes back as it was stored rather than as nil: losing the
value would be a worse answer than handing back the text that is
actually in the database.
Parameters
value(any) — The value as SQLite stored it.conversion(string|nil) — Fromconversion_for().
Returns any
conversions_for()
sql.sqlite.types.conversions_for(declared: list) -> list
The conversions for a whole result, one per column, worked out once so each row does not have to look at the declarations again.
Parameters
declared(list) — The declared types, in column order.
Returns list
decode_row()
sql.sqlite.types.decode_row(values: list, conversions: list) -> list
Applies a result’s conversions to one row of stored values.
Parameters
values(list)conversions(list)
Returns list
2026, Richard Ore and Zuri contributors
sql.statement
import sql.statement
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.statement.*needsimport sql.statement.
A statement compiled once and run many times.
Every engine here splits running a statement into compiling it and executing it, and the compile is the expensive half. A statement run in a loop should pay for it once:
var insert = db.prepare('insert into points (x, y) values (?, ?)')
for point in points {
insert.exec([point.x, point.y])
}
insert.close()
The placeholders are translated once, when the statement is prepared, so the loop does no string work at all.
Classes
Statement
class sql.Statement
A prepared statement.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
sql.Statement(connection, statement, source, sql, style)
Parameters
connection(Connection) — The connection that prepared this.statement(DriverStatement) — The adapter’s own statement.source(string) — The statement as it was written.sql(string) — The statement as the engine received it.style(string) — The driver’s placeholder style.
Statement.source()
sql.Statement.source() -> string
The statement as it was written, with ? or :name in it.
Returns string
Statement.sql()
sql.Statement.sql() -> string
The statement as the engine received it, with the engine’s own placeholders.
Returns string
Statement.columns()
sql.Statement.columns() -> list[dict]
The columns this statement returns, each a dictionary with name and
type. Empty for a statement that returns no rows.
Returns list[dict]
Statement.parameter_count()
sql.Statement.parameter_count() -> number
How many parameters this statement binds.
Returns number
Statement.query()
sql.Statement.query(params) -> ResultSet
Runs the statement and reads its whole result.
Parameters
params(list|dict|nil)
Returns ResultSet
Statement.exec()
sql.Statement.exec(params) -> ExecResult
Runs the statement for its effect.
Parameters
params(list|dict|nil)
Returns ExecResult
Statement.stream()
sql.Statement.stream(params, options) -> Cursor
Runs the statement and returns a cursor over its result.
The cursor holds this statement until it closes, so the statement cannot be run again in the meantime.
Parameters
params(list|dict|nil)options(dict|nil) —{ batch }on engines that fetch in batches; ignored where the engine streams already.
Returns Cursor
Statement.fetch_one()
sql.Statement.fetch_one(params) -> dict|nil
The first row, or nil when the statement returns none.
Parameters
params(list|dict|nil)
Returns dict|nil
Statement.fetch_all()
sql.Statement.fetch_all(params) -> list[dict]
Every row, as dictionaries.
Parameters
params(list|dict|nil)
Returns list[dict]
Statement.fetch_value()
sql.Statement.fetch_value(params, fallback) -> any
The first column of the first row.
Parameters
params(list|dict|nil)fallback(any) — What to return when the statement returns no rows.
Returns any
Statement.close()
sql.Statement.close()
Releases the statement. Safe to call more than once.
Statement.is_closed()
sql.Statement.is_closed() -> bool
Whether this statement has been closed.
Returns bool
Statement.to_string()
sql.Statement.to_string()
2026, Richard Ore and Zuri contributors
sql.transaction
import sql.transaction
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.transaction.*needsimport sql.transaction.
Transactions, and the nesting that savepoints make possible.
The shape worth using is the closure form, because it cannot be got wrong:
db.transaction(@(tx) {
tx.exec('update accounts set balance = balance - ? where id = ?', [100, 1])
tx.exec('update accounts set balance = balance + ? where id = ?', [100, 2])
})
It commits when the closure returns and rolls back when the closure raises, and there is no path through it that leaves a transaction open. The manual form exists for the cases the closure form cannot express, such as a transaction whose lifetime is a request rather than a block.
Nesting
No engine here supports a transaction inside a transaction, but both
support savepoints, which is the same thing under a different name. A
transaction() inside another takes a savepoint, so a helper that opens
one works the same whether it was called on its own or from inside a
larger piece of work. Only the outermost actually commits.
Classes
Transaction
class sql.Transaction
An open transaction.
It carries the same query, exec and CRUD methods a connection does, so code inside a transaction reads the same as code outside one. Every statement it runs goes to the connection the transaction is on.
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
connection | Connection | The connection this transaction is running on. |
nested | bool | Whether this transaction is a savepoint inside another rather than a transaction of its own. |
Constructor
sql.Transaction(connection, nested)
Parameters
connection(Connection)nested(bool)
Transaction.is_finished()
sql.Transaction.is_finished() -> bool
Whether this transaction has already committed or rolled back.
Returns bool
Transaction.query()
sql.Transaction.query(sql: string, params) -> ResultSet
Runs a query on this transaction’s connection.
Parameters
sql(string)params(list|dict|nil)
Returns ResultSet
Transaction.exec()
sql.Transaction.exec(sql: string, params) -> ExecResult
Runs a statement for its effect.
Parameters
sql(string)params(list|dict|nil)
Returns ExecResult
Transaction.fetch_one()
sql.Transaction.fetch_one(sql: string, params) -> dict|nil
The first row of a query, or nil.
Parameters
sql(string)params(list|dict|nil)
Returns dict|nil
Transaction.fetch_all()
sql.Transaction.fetch_all(sql: string, params) -> list[dict]
Every row of a query.
Parameters
sql(string)params(list|dict|nil)
Returns list[dict]
Transaction.fetch_value()
sql.Transaction.fetch_value(sql: string, params, fallback) -> any
The first column of the first row.
Parameters
sql(string)params(list|dict|nil)fallback(any)
Returns any
Transaction.fetch_column()
sql.Transaction.fetch_column(sql: string, params, column) -> list
One column’s values, as a list.
Parameters
sql(string)params(list|dict|nil)column(string|number|nil) — Which column, the first by default.
Returns list
Transaction.stream()
sql.Transaction.stream(sql: string, params, options) -> Cursor
Runs a query and returns a cursor over its result rather than reading all of it. The cursor belongs to the transaction’s connection, so it has to be read to the end or closed before the transaction commits.
Parameters
sql(string)params(list|dict|nil)options(dict|nil) —{ batch }on engines that fetch in batches.
Returns Cursor
Transaction.prepare()
sql.Transaction.prepare(sql: string) -> Statement
Compiles a statement for repeated use inside this transaction.
Parameters
sql(string)
Returns Statement
Transaction.exec_script()
sql.Transaction.exec_script(sql: string)
Runs one or more statements for their effect, as a script: a schema file, a migration, or a batch of settings. It binds no parameters and returns no rows.
On PostgreSQL and SQLite the whole script commits or rolls back with the transaction. MySQL commits a statement that changes a table’s definition as it runs it, whatever transaction it is in, so a script of those is only as atomic as MySQL makes it.
Parameters
sql(string)
Transaction.insert()
sql.Transaction.insert(table: string, values: dict, options) -> any
Inserts a row and returns its id.
Parameters
table(string)values(dict)options(dict|nil)
Returns any
Transaction.insert_many()
sql.Transaction.insert_many(table: string, rows: list) -> number
Inserts several rows in one statement.
Parameters
table(string)rows(list) — Dictionaries of column to value.
Returns number — How many rows were inserted.
Transaction.update()
sql.Transaction.update(table: string, values: dict, where) -> number
Updates the rows matching where.
Parameters
table(string)values(dict)where(dict|nil)
Returns number
Transaction.delete()
sql.Transaction.delete(table: string, where) -> number
Deletes the rows matching where.
Parameters
table(string)where(dict|nil)
Returns number
Transaction.find()
sql.Transaction.find(table: string, where, options) -> ResultSet
Selects the rows matching where.
Parameters
table(string)where(dict|nil)options(dict|nil) —columns,order,limitandoffset.
Returns ResultSet
Transaction.find_one()
sql.Transaction.find_one(table: string, where, options) -> dict|nil
The first row matching where, or nil.
Parameters
table(string)where(dict|nil)options(dict|nil)
Returns dict|nil
Transaction.count()
sql.Transaction.count(table: string, where) -> number
How many rows match where.
Parameters
table(string)where(dict|nil)
Returns number
Transaction.commit()
sql.Transaction.commit()
Commits this transaction, or releases its savepoint when it is nested.
Raises TransactionError if it has already finished.
Transaction.rollback()
sql.Transaction.rollback()
Rolls this transaction back, or undoes its savepoint when it is nested.
Raises TransactionError if it has already finished.
Transaction.to_string()
sql.Transaction.to_string()
2026, Richard Ore and Zuri contributors
sql.types
import sql.types
sqllifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledsql.types.*needsimport sql.types.
How Zuri values and database values correspond, for the parts that are the same whichever engine is underneath.
Databases agree on very little. They all store numbers and text; past that, one has a native JSON type and another stores JSON in a text column, one has a date type with a time zone and another has no date type at all. The adapters differ where the engines differ, and share the conversions that do not have to.
The correspondence
| Zuri | Database |
|---|---|
nil | NULL |
bool | a boolean where the engine has one, otherwise 0 and 1 |
number | an integer where the value is whole, otherwise a float |
bigint | a 64 bit integer, or an error if it will not fit in one |
string | text |
bytes | a blob |
date.Date | a timestamp, or ISO 8601 text on an engine with no date type |
Time | a time interval on an engine that has one, otherwise text |
list, dict | JSON, native where the engine has it and text where it does not |
A value of any other type is refused rather than guessed at. An instance of a class in particular has no obvious storage form, and inventing one would make the round trip lossy in a way that only showed up on the way back out.
Time
Timestamps written as text use ISO 8601 with microseconds and an
explicit offset, which every engine here parses and which sorts
correctly as text. A date.Date carries an offset of its own, so
nothing is assumed about the local zone at either end.
Constants
ISO_FORMAT
sql.types.ISO_FORMAT = 'Y-m-d\TH:i:s.uP'
The format this module writes timestamps in: ISO 8601 with microsecond precision and an explicit UTC offset.
ISO_DATE_FORMAT
sql.types.ISO_DATE_FORMAT = 'Y-m-d'
The same, without the time, for a column that holds only a date.
Functions
is_temporal()
sql.types.is_temporal(value) -> bool
Whether value is a date.Date.
Parameters
value(any)
Returns bool
is_interval()
sql.types.is_interval(value) -> bool
Whether value is a Time.
Parameters
value(any)
Returns bool
is_structured()
sql.types.is_structured(value) -> bool
Whether value is something this module stores as JSON.
Parameters
value(any)
Returns bool
to_iso()
sql.types.to_iso(value) -> string
Formats a date.Date as ISO 8601 text.
Parameters
value(date.Date)
Returns string
from_iso()
sql.types.from_iso(text) -> date.Date|nil
Reads ISO 8601 text back into a date.Date.
Accepts what the engines actually write, which is more than one shape:
with or without a T, with or without fractional seconds, and with or
without an offset. Text that is not a timestamp at all comes back as
nil rather than raising, so a column whose declared type promises a
date but whose contents do not can still be read.
Parameters
text(string)
Returns date.Date|nil
to_json()
sql.types.to_json(value) -> string
Encodes value as JSON text.
Parameters
value(list|dict)
Returns string
from_json()
sql.types.from_json(text) -> any
Decodes JSON text, returning nil for anything that will not parse.
Parameters
text(string)
Returns any
flatten()
sql.types.flatten(value) -> nil|bool|number|bigint|string|bytes
Reduces value to something an engine with no richer type can store: a
timestamp becomes ISO text, a list or dictionary becomes JSON text, and
everything else is already storable and passes through.
Adapters for engines that do have those types encode them natively instead and never call this.
Parameters
value(any)
Returns nil|bool|number|bigint|string|bytes
Raises QueryError for a value with no storable form.
time()
sql.time(hours, minutes, seconds, microseconds, negative) -> Time
Builds a Time.
Parameters
hours(number|nil)minutes(number|nil)seconds(number|nil)microseconds(number|nil)negative(bool|nil)
Returns Time
time_from_seconds()
sql.time_from_seconds(total: number) -> Time
Builds a Time of total seconds, splitting it into parts.
Parameters
total(number) — Negative for a span that runs backwards.
Returns Time
parse_time()
sql.parse_time(text) -> Time|nil
Reads a SQL TIME literal.
Accepts HH:MM:SS, HH:MM, and either with fractional seconds, in each
case optionally signed. Text that is not a time comes back as nil
rather than raising, matching from_iso().
Parameters
text(string)
Returns Time|nil
integer_from_text()
sql.types.integer_from_text(text) -> number|bigint|nil
Reads an integer, however long, as a number where one holds it exactly and a bigint where it does not.
Parameters
text(string) — An optionally signed run of digits.
Returns number|bigint|nil — nil where text is not an integer.
Classes
Time
class sql.Time
A signed span of time, as a SQL TIME column holds one.
A TIME is not a point in the day. MySQL’s range runs from
-838:59:59.999999 to 838:59:59.999999, so a value can be negative
and can exceed twenty four hours, and reading one into a date.Date
would quietly lose both of those. It is a duration: the length of a
shift, the lateness of a delivery, the gap between two events.
The parts are kept as they were written rather than normalised, so a
value built as 90 minutes stays 90 minutes and does not silently become
an hour and a half. total_seconds() is there for arithmetic and
comparison, and compare() uses it, so two values that mean the same
length compare equal whichever way each was built.
Fields
| Field | Type | Description |
|---|---|---|
negative | bool | Whether the span runs backwards. |
hours | number | Whole hours. |
minutes | number | |
seconds | number | |
microseconds | number | Fractional seconds, in millionths. |
Constructor
sql.Time(hours, minutes, seconds, microseconds, negative)
Parameters
hours(number|nil)minutes(number|nil)seconds(number|nil)microseconds(number|nil)negative(bool|nil)
Time.total_seconds()
sql.Time.total_seconds() -> number
The whole span in seconds, negative where the span is.
Returns number — Fractional where there are microseconds.
Time.is_negative()
sql.Time.is_negative() -> bool
Whether the span runs backwards.
A zero span is never negative, whichever way it was built.
Returns bool
Time.is_zero()
sql.Time.is_zero() -> bool
Whether the span is of no length.
Returns bool
Time.compare()
sql.Time.compare(other) -> number
Orders this span against another by length.
Parameters
other(Time)
Returns number — Negative, zero or positive.
Time.equals()
sql.Time.equals(other) -> bool
Whether two spans are of the same length, however each was built.
Parameters
other(any)
Returns bool
Time.to_string()
sql.Time.to_string() -> string
The span as a SQL TIME literal, such as '-12:30:00'.
Microseconds appear only where there are some, which is the same choice the engines make when they print one.
Returns string
2026, Richard Ore and Zuri contributors
mail
import mail
Mail, from the message to the socket.
mail builds and reads messages, and speaks the three protocols that
move them: SMTP to send, IMAP and POP3 to read. It also runs the other
end, with an SMTP server that accepts mail and an IMAP server that
serves it. All of it is written in Zuri.
import mail
var note = mail.message()
.set_from('reports@example.com')
.add_to('ann@example.com')
.set_subject('Quarterly report')
.set_text('The numbers are in.')
mail.send('smtp://mail.example.com', note, {
username: 'reports',
password: secret,
})
What is here
message() | starts a message, built up one call at a time |
parse() | reads one back |
send() | sends one, opening and closing the connection |
Message | a message, or one part of one |
Address | one address and the name written with it |
Headers | the header block, in order and case-insensitive |
SmtpClient | a connection to a server that sends mail |
ImapClient | a connection to a server that stores it |
Pop3Client | the older way of collecting it |
SmtpServer | the receiving end of SMTP |
ImapServer | the serving end of IMAP |
MailStore | where an ImapServer keeps mail |
MailError | the root of every error raised here |
Reading mail
import mail
var inbox = mail.imap('imaps://mail.example.com', {
username: 'ann',
password: secret,
})
inbox.select('INBOX')
for id in inbox.search('UNSEEN') {
echo inbox.fetch_message(id).subject()
}
inbox.logout()
Running a server
The servers are the same shape as http.HttpServer: bind, listen, and
hand each connection to a worker. An SmtpServer decides what to accept
through handlers, and an ImapServer answers from a MailStore, of
which two ship: one on disk in Maildir format and one in memory.
Security
Every client can start in TLS or negotiate it with STARTTLS, and
refuses to send credentials over a connection that is neither.
mail.dkim signs outgoing mail and verifies incoming mail.
The mail API
Every public name in mail, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
mail.ALLOWED_FLAGS | constant | |
mail.Address | class | One address, with the display name that was written alongside it. |
mail.Attachment | class | Something a message carries alongside what it says: a file to offer, or an image the HTML shows. |
mail.AuthenticationError | class | The credentials were not accepted, or no mechanism both ends understand was on offer. |
mail.BASE_CAPABILITIES | constant | |
mail.BodyPart | class | What one message is made of, without fetching it: the type of each part, its size, and where in the message… |
mail.ConnectionClosed | class | The connection closed while a conversation was still going on, or a command was issued on one that had… |
mail.ContentDisposition | class | What a part is for: shown where it sits, or offered as a file. |
mail.ContentType | class | A media type and the parameters that came with it. |
mail.DEFAULT_CHARSET | constant | |
mail.DEFAULT_MAX_RECIPIENTS | constant | |
mail.DEFAULT_MAX_SIZE | constant | |
mail.DEFAULT_TIMEOUT | constant | |
mail.DEFAULT_TYPE | constant | |
mail.DELIMITER | constant | |
mail.Entry | class | One message in the mailbox, as list() reports it. |
mail.Envelope | class | The addresses and dates out of a message’s headers, as the server parsed them, so a list of messages can be… |
mail.Group | class | A named group of addresses, as Managers: ann@example.com, bob@example.com; writes one. |
mail.Headers | class | The headers of a message or of one of its parts. |
mail.IMAP_SCHEMES | constant | |
mail.INDEX_FILE | constant | |
mail.INFO_SEPARATOR | constant | |
mail.ImapClient | class | A connection to a server that stores mail. |
mail.ImapError | class | The IMAP server answered a command with NO or BAD. |
mail.ImapServer | class | A server that serves mail. |
mail.ImapSession | class | One connection, and what it has got as far as doing. |
mail.MAILDIR_FLAGS | constant | |
mail.MAILDIR_PARTS | constant | |
mail.MAX_DATA_LINE | constant | |
mail.MAX_ERRORS | constant | |
mail.MAX_REPLY_LINE | constant | |
mail.MECHANISMS | constant | |
mail.MailError | class | Base class for every error this module raises. |
mail.MailStore | class | What a store has to be able to do. |
mail.Mailbox | class | An open mailbox, and what the server said about it when it opened. |
mail.MailboxError | class | Something went wrong in a mail store: a mailbox that does not exist, one that cannot be created, a message… |
mail.MailboxInfo | class | One mailbox as list() reports it: its name, the character that separates the levels of it, and what the… |
mail.MaildirStore | class | A store that keeps mail on disk in the Maildir layout. |
mail.MemoryStore | class | A store that keeps everything in the process and nothing on disk. |
mail.Message | class | One message, or one part of one. |
mail.MessageError | class | The bytes handed over are not a message, or are one that contradicts itself: a header with no colon in it, a… |
mail.MessageInfo | class | What a FETCH returned about one message. |
mail.POP3_SCHEMES | constant | |
mail.Pop3Client | class | A connection to a server that hands mail over. |
mail.Pop3Error | class | The POP3 server answered a command with -ERR. |
mail.ProtocolError | class | The server said something the protocol does not allow: a greeting that is not a greeting, a response with no… |
mail.Reply | class | One reply from the server: its code, and whatever it said with it. |
mail.SMTP_SCHEMES | constant | |
mail.STATES | constant | |
mail.SmtpClient | class | A connection to a server that sends mail. |
mail.SmtpError | class | The server rejected a command, and said so with a reply code. |
mail.SmtpPermanentError | class | A 5xx reply: the server will not accept the message, and trying again changes nothing. |
mail.SmtpServer | class | A server that accepts mail. |
mail.SmtpSession | class | One connection, and everything known about it so far. |
mail.SmtpTransientError | class | A 4xx reply: the server could not accept the message now, and the sender should try again later. |
mail.StateError | class | A command was issued that makes no sense in the state the connection is in: a fetch before a mailbox has been… |
mail.StoredMessage | class | One message in a store. |
mail.address.address | function | Builds an address without parsing anything. |
mail.address.format_list | function | Writes a list of addresses as a header value. |
mail.address.parse | function | Reads exactly one address. |
mail.address.parse_groups | function | Reads every address in a header value, keeping the groups. |
mail.address.parse_list | function | Reads every address in a header value. |
mail.attachment | function | Starts an attachment from what is in it. |
mail.dkim.ALGORITHMS | constant | The signing algorithms this implements. |
mail.dkim.CANONICALISATIONS | constant | The canonicalisations this implements, on either headers or body. |
mail.dkim.DEFAULT_HEADERS | constant | The headers signed when the signer is not told which to sign. |
mail.dkim.DkimError | class | Raised when a signature cannot be built: an algorithm this does not implement, a key that will not parse, a… |
mail.dkim.Result | class | What checking one signature came to. |
mail.dkim.Signature | class | One DKIM-Signature header, read into its parts. |
mail.dkim.Signer | class | Signs outgoing messages for one domain with one key. |
mail.dkim.canonicalise_body | function | Canonicalises a body. |
mail.dkim.canonicalise_header | function | Canonicalises one header. |
mail.dkim.is_signed | function | Whether a message carries at least one signature that checks out. |
mail.dkim.public_key_pem | function | Turns the p= value of a key record into a PEM the crypto module will accept. |
mail.dkim.verify | function | Checks every signature on a message. |
mail.encoding.TRANSFER_ENCODINGS | constant | |
mail.encoding.decode_base64 | function | Decodes base64, ignoring the line breaks and stray whitespace a message carries it with. |
mail.encoding.decode_body | function | Decodes a part’s body the way its Content-Transfer-Encoding says. |
mail.encoding.decode_parameters | function | Puts the parameters of a header back together. |
mail.encoding.decode_quoted_printable | function | Decodes quoted-printable. |
mail.encoding.decode_text | function | Turns bytes into text, reading them as the character set names. |
mail.encoding.decode_words | function | Reads the encoded words out of a header value and puts back the text they stand for. |
mail.encoding.encode_base64 | function | Encodes data as base64, wrapped into lines. |
mail.encoding.encode_body | function | Encodes a part’s body the way its Content-Transfer-Encoding says. |
mail.encoding.encode_parameter | function | Writes one parameter of a header, choosing the form its value needs. |
mail.encoding.encode_quoted_printable | function | Encodes data as quoted-printable. |
mail.encoding.encode_word | function | Encodes text as one or more RFC 2047 encoded words, so that a header can carry characters a header is not… |
mail.encoding.guess_encoding | function | Picks the transfer encoding a body should use. |
mail.encoding.is_ascii | function | Whether every byte is plain ASCII, which decides whether a header or a body needs encoding at all. |
mail.format_addresses | function | Writes a list of addresses as a header value. |
mail.headers.ADDRESS_HEADERS | constant | |
mail.headers.LINE_WIDTH | constant | |
mail.headers.fold | function | Writes one header, folded so that no line runs past the width. |
mail.headers.unfold | function | |
mail.imap | function | Opens a connection to a server that stores mail. |
mail.message | function | Starts a message. |
mail.multipart | function | |
mail.new_message_id | function | An identifier for a message, unique enough that no two ever collide. |
mail.parse | function | Reads a message. |
mail.parse_address | function | Reads one address out of text. |
mail.parse_address_groups | function | Reads every address out of text, keeping the groups as groups. |
mail.parse_address_list | function | Reads every address out of text, with any groups flattened into their members. |
mail.parser.Literal | class | A run of bytes a server sent as a literal. |
mail.parser.Reader | class | Reads responses off a connection, one at a time. |
mail.parser.Response | class | One response from the server. |
mail.parser.STATUSES | constant | |
mail.parser.SYSTEM_FLAGS | constant | |
mail.parser.quote | function | Writes a value the way a command has to carry it. |
mail.parser.tokenize | function | Reads a run of IMAP values out of text. |
mail.pool.Cluster | class | A running pool: the socket, the workers, and the loop feeding them. |
mail.pool.serve | function | Starts a pool and runs it until something closes it. |
mail.pool.start | function | Starts a pool without running the accept loop, so the address is known before the first connection. |
mail.pool.worker_main | function | What one worker isolate runs: build a server of its own, then serve whatever connections the acceptor hands… |
mail.pop3 | function | Opens a connection to a server that hands mail over. |
mail.sasl.Anonymous | class | ANONYMOUS: no identity at all, with an optional note saying who is knocking. |
mail.sasl.CramMd5 | class | CRAM-MD5: a keyed digest of the server’s challenge, so the password never travels. |
mail.sasl.External | class | EXTERNAL: the connection already established who this is, usually with a client certificate. |
mail.sasl.Login | class | LOGIN: the username and the password again, one prompt at a time. |
mail.sasl.Mechanism | class | What every mechanism looks like from the outside. |
mail.sasl.NEEDS_TLS | constant | The mechanisms that put the password itself on the wire, and so must never be used without TLS. |
mail.sasl.OAuthBearer | class | OAUTHBEARER: the standardised form of the same idea, from RFC 7628. |
mail.sasl.PREFERENCE | constant | The mechanisms this implements, strongest first, which is the order a client picks from what a server offers. |
mail.sasl.Plain | class | PLAIN: the username and the password, separated by zero bytes. |
mail.sasl.Scram | class | SCRAM-SHA-1 and SCRAM-SHA-256: the password is never sent, the server never has to store it, and the… |
mail.sasl.XOAuth2 | class | XOAUTH2: a bearer token rather than a password, which is what Google and Microsoft accept. |
mail.sasl.choose | function | Picks the mechanism to use. |
mail.sasl.of | function | Builds a mechanism by name. |
mail.sasl.prepare | function | Prepares a username or password the way RFC 4013 says to, so that two spellings of the same characters… |
mail.sasl.response | function | The answer to a CRAM-MD5 challenge. |
mail.send | function | Sends one message, opening the connection and closing it again. |
mail.smtp | function | Opens a connection to a server that sends mail. |
mail.stream.LineStream | class | A connection that reads and writes lines. |
mail.stream.MAX_LINE | constant | |
mail.stream.TLS_MODES | constant | How TLS is treated on a connection that did not start encrypted: require refuses to go on without it,… |
mail.stream.connect | function | Opens a connection to a host, in TLS or in the clear. |
mail.stream.endpoint | function | Reads a connection string into the pieces needed to open it. |
mail.stream.start_tls | function | Wraps a connection that started in the clear in TLS, which is what every STARTTLS comes down to. |
mail.text_part | function | Builds one text part. |
Submodules
| Module | Reached as | Summary |
|---|---|---|
mail.address | mail.address.* | Reading and writing the addresses in a mail header. |
mail.content | mail.* | The two headers that say what a part holds and what to do with it. |
mail.dkim | mail.dkim.* | DomainKeys Identified Mail: signing outgoing messages so a receiver can tell they came from the domain they… |
mail.encoding | mail.encoding.* | The encodings a message uses to get eight bit data through a seven bit pipe. |
mail.errors | mail.* | Every error the mail stack raises, under one root. |
mail.headers | mail.headers.* | The header block of a message: what is in it, in what order, and how it is written back out. |
mail.imap | mail.* | IMAP: reading mail where it is kept, rather than taking it away. |
mail.message | mail.* | A mail message: its headers, its body, and the tree of parts a body turns into once there is more than one… |
mail.pool | mail.pool.* | Running a mail server on more than one connection at a time. |
mail.pop3 | mail.* | POP3: the client end of collecting mail. |
mail.sasl | mail.sasl.* | The authentication mechanisms all three mail protocols share. |
mail.smtp | mail.* | SMTP: the protocol that moves mail from where it was written to where it is kept. |
mail.stream | mail.stream.* | A line-oriented connection, which is what all three mail protocols are underneath. |
Functions
parse_address()
mail.parse_address(text: string) -> Address
Reads one address out of text.
import mail
echo mail.parse_address('Ann <ann@example.com>').address
ann@example.com
Parameters
text(string)
Returns Address
Raises MessageError if the text holds no address, or several.
parse_address_list()
mail.parse_address_list(text: string) -> list
Reads every address out of text, with any groups flattened into their members.
Parameters
text(string)
Returns list — of Address
parse_address_groups()
mail.parse_address_groups(text: string) -> list
Reads every address out of text, keeping the groups as groups.
Parameters
text(string)
Returns list — of Address and Group
format_addresses()
mail.format_addresses(people: list) -> string
Writes a list of addresses as a header value.
Parameters
people(list) —Addressvalues,Groupvalues, or text to be parsed first.
Returns string
send()
mail.send(url: string, note, options: ?dict) -> Reply
Sends one message, opening the connection and closing it again.
This is the whole of sending mail for a program that sends one at a
time. A program sending many wants an SmtpClient of its own, kept open
across all of them.
import mail
mail.send('smtp://mail.example.com', mail.message()
.set_from('reports@example.com')
.add_to('ann@example.com')
.set_subject('Quarterly report')
.set_text('The numbers are in.'),
{ username: 'reports', password: secret })
Parameters
url(string) —smtp://hostorsmtps://host.note(Message)options(?dict) — EverythingSmtpClient.connect()takes, andfromandtoto override the envelope.
Returns Reply — the server’s verdict on the message.
Raises SmtpError if the server refuses any part of it.
smtp()
mail.smtp(url: string, options: ?dict) -> SmtpClient
Opens a connection to a server that sends mail.
Parameters
url(string)options(?dict)
Returns SmtpClient
imap()
mail.imap(url: string, options: ?dict) -> ImapClient
Opens a connection to a server that stores mail.
Parameters
url(string)options(?dict)
Returns ImapClient
pop3()
mail.pop3(url: string, options: ?dict) -> Pop3Client
Opens a connection to a server that hands mail over.
Parameters
url(string)options(?dict)
Returns Pop3Client
2026, Richard Ore and Zuri contributors
mail.address
import mail.address
mail.address.*needsimport mail.address.
Reading and writing the addresses in a mail header.
An address in a header is rarely just an address. It can carry a display
name, the name can be quoted or encoded, either can have comments buried
in it, and a header can list several of them or gather them into a named
group. This module turns all of that into Address objects, and turns
them back into a header a server will accept.
import mail.address
var who = address.parse('"Ore, Richard" <richard@example.com> (author)')
echo who.name
echo who.address
echo who.domain
Ore, Richard
richard@example.com
example.com
Functions
parse_groups()
mail.address.parse_groups(text: string) -> list
Reads every address in a header value, keeping the groups.
Entries that are not addresses at all are left out rather than raising,
because one unusable entry in a To header is no reason to refuse the
whole message.
Parameters
text(string)
Returns list — of Address and Group
parse_list()
mail.address.parse_list(text: string) -> list
Reads every address in a header value.
A group’s members come back alongside the rest, since a program sending mail cares who the recipients are and not how they were gathered.
import mail.address
var people = address.parse_list('Ann <ann@example.com>, bob@example.com')
echo people.map(@(person) => person.address)
[ann@example.com, bob@example.com]
Parameters
text(string)
Returns list — of Address
parse()
mail.address.parse(text: string) -> Address
Reads exactly one address.
Parameters
text(string)
Returns Address
Raises MessageError if text holds no address, or more than one.
address()
mail.address.address(spec: string, name: ?string) -> Address
Builds an address without parsing anything.
Parameters
spec(string) — The address, aslocal@domain.name(?string) — The display name.
Returns Address
format_list()
mail.address.format_list(items: list) -> string
Writes a list of addresses as a header value.
Parameters
items(list) —AddressorGroupvalues, or plain strings, which are parsed first.
Returns string
Classes
Address
class mail.Address
One address, with the display name that was written alongside it.
name is the display name, already decoded from whatever encoding the
header carried it in, and is an empty string when there was none.
address is the address itself, local the part before the @ and
domain the part after it.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.Address(spec: string, name: ?string)
Builds an address.
import mail.address { Address }
echo Address('richard@example.com', 'Richard Ore').to_string()
Richard Ore <richard@example.com>
Parameters
spec(string) — The address, aslocal@domain.name(?string) — The display name. Empty when not given.
Raises MessageError if spec has no local part.
Address.is_routable()
mail.Address.is_routable() -> bool
Whether this address has a domain. An address without one is legal in a header but cannot be delivered to over the network.
Returns bool
Address.to_string()
mail.Address.to_string() -> string
The address as a header would carry it, with the display name quoted or encoded as it needs to be.
Returns string
Address.equals()
mail.Address.equals(other) -> bool
Whether two addresses are the same one.
The domain is compared without regard to case, since domains are case-insensitive. The local part is compared exactly, since it is the receiving server’s to interpret and some of them do distinguish case.
Parameters
other(Address)
Returns bool
Group
class mail.Group
A named group of addresses, as Managers: ann@example.com, bob@example.com;
writes one.
Groups are rare, and a program that does not care about them can use
parse_list(), which hands back the members and forgets the name.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.Group(name: string, members: list)
Parameters
name(string) — The group’s name.members(list) — TheAddressvalues in it, possibly empty.
Group.to_string()
mail.Group.to_string()
2026, Richard Ore and Zuri contributors
mail.content
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.contenton its own works too and reaches the same definitions.
The two headers that say what a part holds and what to do with it.
Content-Type names the media type and carries the parameters that go
with it: the character set of text, the boundary of a multipart, the
name of a file. Content-Disposition says whether the part is meant to
be shown where it sits or offered as an attachment, and carries the
filename to offer it under.
import mail.content { ContentType }
var type = ContentType.parse('text/plain; charset="utf-8"; format=flowed')
echo type.mime_type()
echo type.charset()
echo type.get('format')
text/plain
utf-8
flowed
Constants
DEFAULT_TYPE
mail.DEFAULT_TYPE = 'text/plain'
DEFAULT_CHARSET
mail.DEFAULT_CHARSET = 'us-ascii'
Classes
ContentType
class mail.ContentType
A media type and the parameters that came with it.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.ContentType(type: string, subtype: string, parameters: ?dict)
Parameters
type(string) — The type, such astext.subtype(string) — The subtype, such asplain.parameters(?dict) — The parameters, already decoded.
ContentType.parse()
mail.ContentType.parse(text: ?string) -> ContentType
Reads a Content-Type header.
A header that is missing or empty gives text/plain, which is what a
part with no type is defined to be.
Parameters
text(?string)
Returns ContentType
ContentType.mime_type()
mail.ContentType.mime_type() -> string
The type and subtype together, as text/plain.
Returns string
ContentType.get()
mail.ContentType.get(name: string, fallback) -> any
One parameter’s value.
Parameters
name(string) — Matched without regard to case.fallback(?any) — Returned when the parameter is absent.
Returns any
ContentType.charset()
mail.ContentType.charset() -> string
The character set of a text part.
Returns string — us-ascii when the header does not say, which is
the defined default.
ContentType.boundary()
mail.ContentType.boundary() -> string|nil
The boundary that separates the parts of a multipart, or nil when this
is not one.
Returns string|nil
ContentType.name()
mail.ContentType.name() -> string|nil
The filename this part suggests, taken from the name parameter.
Content-Disposition is the header that is supposed to carry it, and
Message.filename() looks there first.
Returns string|nil
ContentType.is_multipart()
mail.ContentType.is_multipart() -> bool
Whether this part contains other parts.
Returns bool
ContentType.is_text()
mail.ContentType.is_text() -> bool
Whether this part is text, and so has a character set worth applying.
Returns bool
ContentType.to_string()
mail.ContentType.to_string() -> string
The header value, with every parameter written in the form it needs.
Returns string
ContentDisposition
class mail.ContentDisposition
What a part is for: shown where it sits, or offered as a file.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.ContentDisposition(kind: string, parameters: ?dict)
Parameters
kind(string) —inlineorattachment.parameters(?dict)
ContentDisposition.parse()
mail.ContentDisposition.parse(text: ?string) -> ContentDisposition
Reads a Content-Disposition header.
Parameters
text(?string)
Returns ContentDisposition
ContentDisposition.get()
mail.ContentDisposition.get(name: string, fallback) -> any
One parameter’s value.
Parameters
name(string)fallback(?any)
Returns any
ContentDisposition.filename()
mail.ContentDisposition.filename() -> string|nil
The filename the part should be saved under, or nil.
Returns string|nil
ContentDisposition.is_attachment()
mail.ContentDisposition.is_attachment() -> bool
Whether the part is meant to be offered as a file rather than shown in place.
Returns bool
ContentDisposition.to_string()
mail.ContentDisposition.to_string()
2026, Richard Ore and Zuri contributors
mail.dkim
import mail
mail.dkim, soimport mailis enough and the names are called asmail.dkim.*.import mail.dkimreaches the same definitions directly.
DomainKeys Identified Mail: signing outgoing messages so a receiver can tell they came from the domain they claim to, and checking the signatures on incoming ones.
A signature covers the body and a chosen set of headers. The public key lives in the domain’s own DNS, under the selector the signature names, which is what ties the message to the domain.
import mail
import mail.dkim { Signer }
var signer = Signer('example.com', 'default', private_key)
signer.sign(note)
mail.send('smtp://mail.example.com', note)
Verification looks the key up through net.resolver unless handed
something else to look it up with:
import mail.dkim
for result in dkim.verify(incoming) {
echo '${result.domain}: ${result.valid ? 'ok' : result.reason}'
}
What is covered
Both canonicalisations, simple and relaxed, on headers and on
bodies, and both signing algorithms in use: rsa-sha256 and the
ed25519-sha256 of RFC 8463.
A signature says nothing about who wrote the message, only that the domain that signed it takes responsibility for it. Deciding what a valid signature from a given domain is worth is a policy question, and the answer is usually DMARC.
Constants
ALGORITHMS
mail.dkim.ALGORITHMS: list = [...]
The signing algorithms this implements.
CANONICALISATIONS
mail.dkim.CANONICALISATIONS: list = [...]
The canonicalisations this implements, on either headers or body.
simple covers the bytes as they are, which means any change at all on
the way breaks the signature. relaxed forgives the whitespace and
folding changes a mail server may make in passing, and is what almost
everything uses.
DEFAULT_HEADERS
mail.dkim.DEFAULT_HEADERS: list = [...]
The headers signed when the signer is not told which to sign.
A header that is not in the message is skipped rather than signed as empty, and one that is listed twice is signed twice, which is how a signature says “there was exactly one of these”.
Functions
canonicalise_body()
mail.dkim.canonicalise_body(raw, method: string) -> bytes
Canonicalises a body.
Parameters
raw(string|bytes)method(string) —simpleorrelaxed.
Returns bytes
Raises DkimError if method is neither.
canonicalise_header()
mail.dkim.canonicalise_header(name: string, value: string, method: string)
Canonicalises one header.
Parameters
name(string)value(string) — The value as it was written, folding and all.method(string) —simpleorrelaxed.
Returns — string, without the line break that ends it.
Raises DkimError if method is neither.
public_key_pem()
mail.dkim.public_key_pem(encoded: string, algorithm: string) -> string
Turns the p= value of a key record into a PEM the crypto module will
accept.
An RSA key is published as the whole SubjectPublicKeyInfo structure and only needs wrapping. An Ed25519 key is published as the 32 raw bytes, and has that structure put around it here.
Parameters
encoded(string) — The base64 from the record’sp=tag.algorithm(string) — One ofALGORITHMS.
Returns string
Raises DkimError if the key will not decode.
verify()
mail.dkim.verify(message, lookup) -> list
Checks every signature on a message.
One result comes back per DKIM-Signature header, in the order they
appear, whether it passed or not. A message with no signatures gives an
empty list, which is not a failure: it is a message nobody signed.
import mail.dkim
for result in dkim.verify(incoming) {
echo '${result.domain}: ${result.valid ? 'pass' : result.reason}'
}
Parameters
message(Message)lookup(?function) — Given a name, returns the text records published under it.net.resolveris used when not given.
Returns list — of Result
Note: A message that was parsed is checked against the bytes it was parsed from, which is the only thing a signature can be checked against. Changing a parsed message and checking it again reports on the message that arrived, not the one now in hand.
is_signed()
mail.dkim.is_signed(message, lookup) -> bool
Whether a message carries at least one signature that checks out.
Parameters
message(Message)lookup(?function)
Returns bool
Classes
DkimError
class mail.dkim.DkimError < MailError
Raised when a signature cannot be built: an algorithm this does not implement, a key that will not parse, a canonicalisation that is not one of the two.
- printable — has a
@to_string(), soechoandprint()show something useful
DkimError.to_string()
mail.dkim.DkimError.to_string()
Signature
class mail.dkim.Signature
One DKIM-Signature header, read into its parts.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.dkim.Signature(tags: dict, raw: ?string)
Parameters
tags(dict) — The tags of the header, by name.raw(?string) — The header value it was read from.
Signature.parse()
mail.dkim.Signature.parse(value: string) -> Signature
Reads a DKIM-Signature header value.
Parameters
value(string)
Returns Signature
Signature.record_name()
mail.dkim.Signature.record_name() -> string
The name the public key is published under.
Returns string
Signature.to_string()
mail.dkim.Signature.to_string()
Signer
class mail.dkim.Signer
Signs outgoing messages for one domain with one key.
Build one and keep it: a signer holds no connection and no state beyond its key, and signing is the only thing it does.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.dkim.Signer(domain: string, selector: string, private_key: string, options: ?dict)
| option | default | what it does |
|---|---|---|
algorithm | rsa-sha256 | or ed25519-sha256 |
canonicalisation | relaxed/relaxed | headers then body |
headers | DEFAULT_HEADERS | which headers to cover |
identity | none | the i= tag, an address within the domain |
expires_in | none | seconds until the signature stops counting |
timestamp | true | whether to record when it was signed |
Parameters
domain(string) — The domain taking responsibility.selector(string) — Which of the domain’s keys this is.private_key(string) — The key, PEM encoded.options(?dict)
Raises DkimError if an option names something not implemented.
Signer.sign()
mail.dkim.Signer.sign(message) -> string
Signs a message, adding the DKIM-Signature header to the top of it.
The message is signed as it stands, so everything else about it has to be settled first. Changing a signed header afterwards breaks the signature.
Parameters
message(Message)
Returns string — the header value that was added.
Raises DkimError if the key will not sign.
Signer.to_string()
mail.dkim.Signer.to_string()
Result
class mail.dkim.Result
What checking one signature came to.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.dkim.Result(signature, valid: bool, reason: ?string)
Parameters
signature(Signature) — The signature that was checked.valid(bool)reason(?string) — Why not, when it is not valid.
Result.to_string()
mail.dkim.Result.to_string()
2026, Richard Ore and Zuri contributors
mail.encoding
import mail
mail.encoding, soimport mailis enough and the names are called asmail.encoding.*.import mail.encodingreaches the same definitions directly.
The encodings a message uses to get eight bit data through a seven bit pipe.
Mail headers are ASCII and mail bodies were historically ASCII too, so everything else has to be spelled out in it. Bodies use base64 or quoted-printable; headers use the encoded words of RFC 2047 and, for parameters, the continuations of RFC 2231. This module implements all four in both directions, plus the character sets a message is likely to name.
import mail.encoding
echo encoding.encode_word('Grüße', 'utf-8', 'Q')
echo encoding.decode_words('=?utf-8?Q?Gr=C3=BC=C3=9Fe?=')
=?utf-8?Q?Gr=C3=BC=C3=9Fe?=
Grüße
Constants
TRANSFER_ENCODINGS
mail.encoding.TRANSFER_ENCODINGS = [...]
Functions
is_ascii()
mail.encoding.is_ascii(data) -> bool
Whether every byte is plain ASCII, which decides whether a header or a body needs encoding at all.
Parameters
data(string|bytes)
Returns bool
encode_base64()
mail.encoding.encode_base64(data, width: ?number) -> string
Encodes data as base64, wrapped into lines.
Parameters
data(string|bytes)width(?number) — Characters per line,76by default. Pass0for one unbroken line, which is what a header needs.
Returns string
decode_base64()
mail.encoding.decode_base64(text) -> bytes
Decodes base64, ignoring the line breaks and stray whitespace a message carries it with.
Parameters
text(string|bytes)
Returns bytes
Raises MessageError if what is left is not base64.
encode_quoted_printable()
mail.encoding.encode_quoted_printable(data) -> string
Encodes data as quoted-printable.
Bytes that can stand for themselves do; the rest become =XX. Lines are
kept within 76 characters with the soft breaks the encoding defines, and
whitespace at the end of a line is encoded so that a transport which
trims it cannot change what was sent.
import mail.encoding
echo encoding.encode_quoted_printable('café = good')
caf=C3=A9 =3D good
Parameters
data(string|bytes)
Returns string
decode_quoted_printable()
mail.encoding.decode_quoted_printable(text) -> bytes
Decodes quoted-printable.
Tolerant of what real messages contain: a soft break with a bare newline
rather than a carriage return and newline, and a lone = that is not
followed by two hex digits, which is passed through rather than treated
as a failure.
Parameters
text(string|bytes)
Returns bytes
decode_text()
mail.encoding.decode_text(raw, charset: ?string) -> string
Turns bytes into text, reading them as the character set names.
The sets a message is likely to name are understood outright: UTF-8, US-ASCII, the ISO 8859 Latin sets 1 and 15, windows-1252, and UTF-16 in either byte order. A set this does not know is read as UTF-8, which leaves the ASCII in it intact rather than discarding the part that would have been readable.
Parameters
raw(string|bytes)charset(?string) —utf-8when not given.
Returns string
encode_word()
mail.encoding.encode_word(text: string, charset: ?string, method: ?string) -> string
Encodes text as one or more RFC 2047 encoded words, so that a header can carry characters a header is not allowed to contain.
Text that is already ASCII is returned untouched, since a word that encodes nothing only makes the header harder to read.
Long text becomes several words separated by a space, because one word may not exceed 75 characters. Words are split on character boundaries, never in the middle of one.
Parameters
text(string)charset(?string) —utf-8when not given.method(?string) —BorQ. Chosen by what the text looks like when not given:Qwhen most of it is ASCII,Botherwise.
Returns string
decode_words()
mail.encoding.decode_words(text: string) -> string
Reads the encoded words out of a header value and puts back the text they stand for.
Whitespace between two adjacent encoded words is dropped, which is what lets a long subject be split across several of them without a space appearing where none was written.
Anything that is not a well-formed encoded word is left exactly as it
is, including text that merely begins with =?.
import mail.encoding
echo encoding.decode_words('=?utf-8?B?SGVsbG8=?= =?utf-8?B?IHdvcmxk?=')
Hello world
Parameters
text(string)
Returns string
encode_parameter()
mail.encoding.encode_parameter(name: string, value: string) -> list
Writes one parameter of a header, choosing the form its value needs.
A short ASCII value is quoted and written as it is. A value with characters a header cannot carry is written in the extended form of RFC 2231, which names the character set. A long value is split across numbered continuations, because a parameter may not run past the end of a line.
import mail.encoding
echo '; '.join(encoding.encode_parameter('filename', 'report.pdf'))
echo '; '.join(encoding.encode_parameter('filename', 'résumé.pdf'))
filename="report.pdf"
filename*=utf-8''r%C3%A9sum%C3%A9.pdf
Parameters
name(string)value(string)
Returns list — of string, each key=value ready to be joined with
'; '
decode_parameters()
mail.encoding.decode_parameters(raw: dict) -> dict
Puts the parameters of a header back together.
Takes them as they were written, continuations and extended forms and all, and hands back one value per parameter with the pieces joined and the character set applied.
Parameters
raw(dict) — Parameters by the name they were written under, including the*0*and*suffixes.
Returns dict
encode_body()
mail.encoding.encode_body(data, encoding: string) -> string
Encodes a part’s body the way its Content-Transfer-Encoding says.
7bit, 8bit and binary describe the data rather than change it, so
they hand it back as it is.
Parameters
data(string|bytes)encoding(string)
Returns string
Raises MessageError if encoding is not one this understands.
decode_body()
mail.encoding.decode_body(data, encoding: ?string) -> bytes
Decodes a part’s body the way its Content-Transfer-Encoding says.
An encoding this does not understand is treated as 8bit and handed
back untouched, because a part whose encoding cannot be read is still
better delivered than discarded.
Parameters
data(string|bytes)encoding(?string) —7bitwhen not given.
Returns bytes
guess_encoding()
mail.encoding.guess_encoding(data) -> string
Picks the transfer encoding a body should use.
ASCII text that fits inside the line limit needs none. Text that is mostly ASCII is cheaper as quoted-printable, which leaves it readable; anything else is smaller as base64.
Parameters
data(string|bytes)
Returns string — one of 7bit, quoted-printable or base64
2026, Richard Ore and Zuri contributors
mail.errors
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.errorson its own works too and reaches the same definitions.
Every error the mail stack raises, under one root.
MailError is that root. The subclasses separate the cases worth
handling differently: a message that will not parse, a server that
refused, credentials that were not accepted, a connection that went away
mid-conversation.
The protocols each add one of their own, carrying the reply the server actually sent. SMTP splits its into two, because the difference between “not now” and “not ever” is the difference between queueing a message and bouncing it.
Classes
MailError
class mail.MailError < Error
Base class for every error this module raises.
- printable — has a
@to_string(), soechoandprint()show something useful
MailError.to_string()
mail.MailError.to_string()
MessageError
class mail.MessageError < MailError
The bytes handed over are not a message, or are one that contradicts itself: a header with no colon in it, a multipart body whose boundary never appears, a transfer encoding that decodes to nothing.
- printable — has a
@to_string(), soechoandprint()show something useful
MessageError.to_string()
mail.MessageError.to_string()
ProtocolError
class mail.ProtocolError < MailError
The server said something the protocol does not allow: a greeting that is not a greeting, a response with no tag, a line longer than the specification permits.
- printable — has a
@to_string(), soechoandprint()show something useful
ProtocolError.to_string()
mail.ProtocolError.to_string()
ConnectionClosed
class mail.ConnectionClosed < MailError
The connection closed while a conversation was still going on, or a command was issued on one that had already closed.
- printable — has a
@to_string(), soechoandprint()show something useful
ConnectionClosed.to_string()
mail.ConnectionClosed.to_string()
AuthenticationError
class mail.AuthenticationError < MailError
The credentials were not accepted, or no mechanism both ends understand was on offer.
- printable — has a
@to_string(), soechoandprint()show something useful
AuthenticationError.to_string()
mail.AuthenticationError.to_string()
StateError
class mail.StateError < MailError
A command was issued that makes no sense in the state the connection is in: a fetch before a mailbox has been selected, a recipient before a sender.
- printable — has a
@to_string(), soechoandprint()show something useful
StateError.to_string()
mail.StateError.to_string()
SmtpError
class mail.SmtpError < MailError
The server rejected a command, and said so with a reply code.
code is the three digit code, and enhanced the finer grained one
from RFC 3463 when the server sends one, as '5.7.1'. Servers differ in
how much detail they put in text, and none of it is meant to be
matched on.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.SmtpError(message: string, code: number, enhanced: ?string, lines: ?list)
Parameters
message(string)code(number) — The three digit reply code.enhanced(?string) — The enhanced status code, when sent.lines(?list) — Every line of the reply, without its code.
SmtpError.to_string()
mail.SmtpError.to_string()
SmtpTransientError
class mail.SmtpTransientError < SmtpError
A 4xx reply: the server could not accept the message now, and the sender should try again later. A queue retries these.
- printable — has a
@to_string(), soechoandprint()show something useful
SmtpTransientError.to_string()
mail.SmtpTransientError.to_string()
SmtpPermanentError
class mail.SmtpPermanentError < SmtpError
A 5xx reply: the server will not accept the message, and trying again changes nothing. A queue bounces these.
- printable — has a
@to_string(), soechoandprint()show something useful
SmtpPermanentError.to_string()
mail.SmtpPermanentError.to_string()
ImapError
class mail.ImapError < MailError
The IMAP server answered a command with NO or BAD.
status is which of the two it was. NO means the command was
understood and refused; BAD means it was not understood, which points
at the client rather than the request.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.ImapError(message: string, status: string, code: ?string)
Parameters
message(string)status(string) —NOorBAD.code(?string) — The bracketed response code, such asTRYCREATE, when the server sent one.
ImapError.to_string()
mail.ImapError.to_string()
Pop3Error
class mail.Pop3Error < MailError
The POP3 server answered a command with -ERR.
- printable — has a
@to_string(), soechoandprint()show something useful
Pop3Error.to_string()
mail.Pop3Error.to_string()
MailboxError
class mail.MailboxError < MailError
Something went wrong in a mail store: a mailbox that does not exist, one that cannot be created, a message that has gone from under a session that was reading it.
- printable — has a
@to_string(), soechoandprint()show something useful
MailboxError.to_string()
mail.MailboxError.to_string()
2026, Richard Ore and Zuri contributors
mail.headers
import mail.headers
mail.headers.*needsimport mail.headers.
The header block of a message: what is in it, in what order, and how it is written back out.
Mail headers are not a dictionary. The same name can appear more than
once and the order matters, which is why Received traces a route and
why a signature covers the headers it covers. Headers keeps them as
they were, matches names without regard to case, and hands back one
value or all of them as the caller asks.
import mail.headers { Headers }
var headers = Headers()
headers.set('Subject', 'Quarterly report')
headers.add('Received', 'from a.example.com')
headers.add('Received', 'from b.example.com')
echo headers.get('subject', nil)
echo headers.get_all('Received').length()
Quarterly report
2
Constants
LINE_WIDTH
mail.headers.LINE_WIDTH = 78
ADDRESS_HEADERS
mail.headers.ADDRESS_HEADERS = [...]
Functions
unfold()
mail.headers.unfold(text: string)
fold()
mail.headers.fold(name: string, value: string, width: ?number) -> string
Writes one header, folded so that no line runs past the width.
Folding happens at whitespace, which is the only place it is allowed to. A single run of characters with no whitespace in it cannot be folded and is left to overrun, because breaking it would change what it says.
import mail.headers
var folded = headers.fold('Subject', ('word ' * 20).trim(), 40)
echo folded.lines().length()
3
Parameters
name(string)value(string)width(?number) —78when not given.
Returns string — with \r\n between the lines it produced
Classes
Headers
class mail.Headers
The headers of a message or of one of its parts.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.Headers(entries: ?list)
Parameters
entries(?list) — Pairs of[name, value]to start with.
Headers.parse()
mail.Headers.parse(text) -> Headers
Reads a header block.
Lines that continue the one before them are joined back together. A line that is not a header and does not continue one is skipped, which is what lets a message with an mbox separator in front of it still be read.
Parameters
text(string|bytes) — The block, without the blank line that ends it.
Returns Headers
Headers.add()
mail.Headers.add(name: string, value) -> Headers
Adds a header, leaving any already there alone.
Parameters
name(string)value(string)
Returns Headers — itself.
Headers.prepend()
mail.Headers.prepend(name: string, value) -> Headers
Adds a header in front of every other one.
A header that records what happened to a message on its way goes at the
top, so that reading down the block reads backwards through the
message’s history. Received and DKIM-Signature are both added this
way.
Parameters
name(string)value(string)
Returns Headers — itself.
Headers.set()
mail.Headers.set(name: string, value) -> Headers
Sets a header, removing any already there.
Parameters
name(string)value(string)
Returns Headers — itself.
Headers.get()
mail.Headers.get(name: string, fallback) -> string|any
The first value of a header, or fallback when it is not there.
The value comes back as it was written. Use decoded() when it may
carry encoded words.
Parameters
name(string)fallback(?any)
Returns string|any
Headers.get_all()
mail.Headers.get_all(name: string) -> list
Every value of a header, in the order they appear.
Parameters
name(string)
Returns list — of string
Headers.decoded()
mail.Headers.decoded(name: string, fallback) -> string|any
The first value of a header with its encoded words decoded.
Parameters
name(string)fallback(?any)
Returns string|any
Headers.remove()
mail.Headers.remove(name: string) -> number
Removes every instance of a header.
Parameters
name(string)
Returns number — how many were removed.
Headers.contains()
mail.Headers.contains(name: string) -> bool
Whether a header is present.
Parameters
name(string)
Returns bool
Headers.names()
mail.Headers.names() -> list
The names of every header, in order and as they were written. A name that appears more than once appears here more than once.
Returns list — of string
Headers.entries()
mail.Headers.entries() -> list
Every header as a { name, value } dictionary, in order.
Returns list — of dict
Headers.length()
mail.Headers.length() -> number
How many headers there are, counting repeats separately.
Returns number
Headers.is_empty()
mail.Headers.is_empty() -> bool
Returns bool
Headers.clone()
mail.Headers.clone() -> Headers
A copy that can be changed without changing this one.
Returns Headers
Headers.addresses()
mail.Headers.addresses(name: string) -> list
The addresses in a header, across every instance of it.
import mail.headers { Headers }
var headers = Headers([['To', 'Ann <ann@example.com>, bob@example.com']])
echo headers.addresses('to').map(@(person) => person.address)
[ann@example.com, bob@example.com]
Parameters
name(string)
Returns list — of Address
Headers.set_addresses()
mail.Headers.set_addresses(name: string, people) -> Headers
Sets a header to a list of addresses.
Parameters
name(string)people(list|string|Address) — One address or several, asAddressvalues or as text to be parsed.
Returns Headers — itself.
Headers.date()
mail.Headers.date(name: ?string) -> Date|nil
A header read as a date, or nil when it is absent or unreadable.
Parameters
name(?string) —Datewhen not given.
Returns Date|nil
Headers.set_date()
mail.Headers.set_date(name: string, moment) -> Headers
Sets a header to a date, written the way RFC 5322 wants one.
Parameters
name(string)moment(?Date) — The current time when not given.
Returns Headers — itself.
Headers.content_type()
mail.Headers.content_type() -> ContentType
The Content-Type of this part.
Returns ContentType
Headers.content_disposition()
mail.Headers.content_disposition() -> ContentDisposition
The Content-Disposition of this part.
Returns ContentDisposition
Headers.to_string()
mail.Headers.to_string() -> string
The whole block, folded, with every line ended by a carriage return and newline. The blank line that separates the headers from the body is not included.
Returns string
2026, Richard Ore and Zuri contributors
mail.imap
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.imapon its own works too and reaches the same definitions.
IMAP: reading mail where it is kept, rather than taking it away.
Both ends are here. ImapClient talks to a server; ImapServer is one,
answering out of a MailStore of which two ship.
import mail.imap { ImapClient }
var inbox = ImapClient.connect('imaps://mail.example.com', {
username: 'ann',
password: secret,
})
inbox.select('INBOX')
echo inbox.search('UNSEEN', true).length()
inbox.logout()
Submodules
| Module | Reached as | Summary |
|---|---|---|
mail.imap.client | mail.* | The reading end of IMAP. |
mail.imap.parser | import mail.imap.parser | The IMAP grammar: turning what a server says into something a program can read. |
mail.imap.server | mail.* | The serving end of IMAP. |
mail.imap.store | mail.* | Where an ImapServer keeps mail. |
2026, Richard Ore and Zuri contributors
mail.imap.client
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.imap.clienton its own works too and reaches the same definitions.
The reading end of IMAP.
ImapClient connects to a server, opens a mailbox, and reads what is in
it. Unlike POP3, nothing is downloaded that was not asked for: a
message’s headers, one part of it, or the whole of it, as needed.
import mail.imap { ImapClient }
var inbox = ImapClient.connect('imaps://mail.example.com', {
username: 'ann',
password: secret,
})
inbox.select('INBOX')
for id in inbox.search('UNSEEN') {
echo inbox.fetch_message(id).subject()
}
inbox.logout()
Constants
IMAP_SCHEMES
mail.IMAP_SCHEMES = {...}
STATES
mail.STATES = [...]
Classes
Mailbox
class mail.Mailbox
An open mailbox, and what the server said about it when it opened.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.Mailbox(name: string)
Parameters
name(string)
Mailbox.to_string()
mail.Mailbox.to_string()
MailboxInfo
class mail.MailboxInfo
One mailbox as list() reports it: its name, the character that
separates the levels of it, and what the server says about it.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.MailboxInfo(name: string, flags: list, delimiter: ?string)
Parameters
name(string)flags(list) — The attributes, such as\HasChildren.delimiter(?string) — The hierarchy separator, ornilfor a server with no hierarchy at all.
MailboxInfo.is_selectable()
mail.MailboxInfo.is_selectable() -> bool
Whether the server says this name cannot be selected, which is what a folder that only holds other folders looks like.
Returns bool
MailboxInfo.to_string()
mail.MailboxInfo.to_string()
Envelope
class mail.Envelope
The addresses and dates out of a message’s headers, as the server parsed them, so a list of messages can be shown without fetching any of them.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.Envelope(values: list)
Envelope.sent_at()
mail.Envelope.sent_at() -> Date|nil
The date as a Date, or nil when the server sent none or it cannot be
read.
Returns Date|nil
Envelope.to_string()
mail.Envelope.to_string()
BodyPart
class mail.BodyPart
What one message is made of, without fetching it: the type of each part, its size, and where in the message it sits.
section is the number a BODY[...] fetch uses to ask for that part on
its own.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.BodyPart(type: string, subtype: string, parameters: dict, size: ?number, section: string)
Parameters
type(string)subtype(string)parameters(dict)size(?number)section(string)
BodyPart.mime_type()
mail.BodyPart.mime_type() -> string
The media type, as text/plain.
Returns string
BodyPart.walk()
mail.BodyPart.walk() -> list
Every part inside this one and itself, outermost first.
Returns list — of BodyPart
BodyPart.to_string()
mail.BodyPart.to_string()
MessageInfo
class mail.MessageInfo
What a FETCH returned about one message.
Which fields are filled in depends on what was asked for. parts holds
each BODY[...] section that came back, keyed by the section that was
requested.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.MessageInfo(sequence: number)
Parameters
sequence(number) — The message’s position in the mailbox.
MessageInfo.message()
mail.MessageInfo.message() -> Message|nil
The whole message, when the fetch asked for it.
Returns Message|nil
MessageInfo.is_seen()
mail.MessageInfo.is_seen() -> bool
Whether the message has been read.
Returns bool
MessageInfo.to_string()
mail.MessageInfo.to_string()
ImapClient
class mail.ImapClient
A connection to a server that stores mail.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.ImapClient(connection, options: ?dict)
Builds a client around a connection that is already open.
Parameters
connection(LineStream)options(?dict) — Asconnect().
ImapClient.connect()
mail.ImapClient.connect(url: string, options: ?dict) -> ImapClient
Opens a connection and gets as far as being able to select a mailbox.
| option | default | what it does |
|---|---|---|
username, password | none | authenticate when both are given |
token | none | authenticate with a bearer token instead |
tls | require | require, prefer or disable, when not already encrypted |
tls_config | a default one | the trust settings for TLS |
mechanisms | all of them | restricts which are acceptable |
timeout | 60000 | milliseconds to wait on the socket |
Parameters
url(string) —imaps://host,imap://host, or a bare host.options(?dict)
Returns ImapClient
Raises ImapError if the server refuses.
Raises AuthenticationError if it refuses the credentials.
ImapClient.begin()
mail.ImapClient.begin() -> ImapClient
Reads the greeting, negotiates TLS and authenticates.
Returns ImapClient — itself.
Raises ImapError if the greeting says the server is not available.
ImapClient.supports()
mail.ImapClient.supports(name: string) -> bool
Whether the server offers a capability.
Parameters
name(string) — Matched without regard to case.
Returns bool
ImapClient.is_secure()
mail.ImapClient.is_secure() -> bool
Whether the connection is encrypted.
Returns bool
ImapClient.refresh_capabilities()
mail.ImapClient.refresh_capabilities() -> list
Asks the server what it can do, replacing what is known.
Returns list — of string
ImapClient.start_tls()
mail.ImapClient.start_tls() -> ImapClient
Negotiates TLS over a connection that started in the clear, and asks what the server can do again, because nothing it said before the handshake was protected.
Returns ImapClient — itself.
Raises ImapError if the server refuses.
ImapClient.login()
mail.ImapClient.login(mechanism: ?string) -> ImapClient
Authenticates, choosing the strongest mechanism both ends know.
A server may forbid a mechanism outright by advertising LOGINDISABLED,
which is how it says the connection is not private enough; this refuses
to try in that case rather than sending the password anyway.
Parameters
mechanism(?string) — Forces one rather than choosing.
Returns ImapClient — itself.
Raises AuthenticationError if there is no mechanism in common or
the credentials are refused.
ImapClient.authenticate()
mail.ImapClient.authenticate(mechanism) -> ImapClient
Runs one authentication exchange with a mechanism already built.
Parameters
mechanism(Mechanism)
Returns ImapClient — itself.
Raises AuthenticationError if the server refuses.
ImapClient.run()
mail.ImapClient.run(name: string, arguments: list) -> list
Sends a command and reads everything it draws, up to and including the tagged answer.
Arguments are written as they are: a plain string goes unquoted, so
quote anything that needs it with parser.quote() first. A Literal is
sent as a literal, waiting for the server’s permission unless it said
permission is not needed.
Parameters
name(string)arguments(list)
Returns list — of the untagged responses the command produced.
Raises ImapError if the server answers NO or BAD.
ImapClient.on_event()
mail.ImapClient.on_event(listener) -> ImapClient
Calls a function with every untagged response the server sends.
This is how a program watches a mailbox: the server announces new mail
and changed flags whenever it likes, and idle() exists so there is
something to announce them during.
Parameters
listener(?function(1)) —nilstops listening.
Returns ImapClient — itself.
ImapClient.select()
mail.ImapClient.select(name: string) -> Mailbox
Opens a mailbox for reading and writing.
Parameters
name(string)
Returns Mailbox
Raises ImapError if there is no such mailbox.
ImapClient.examine()
mail.ImapClient.examine(name: string) -> Mailbox
Opens a mailbox without being able to change anything in it, which also means reading a message does not mark it read.
Parameters
name(string)
Returns Mailbox
Raises ImapError if there is no such mailbox.
ImapClient.close_mailbox()
mail.ImapClient.close_mailbox() -> ImapClient
Closes the open mailbox, removing the messages marked deleted on the way out.
Returns ImapClient — itself.
ImapClient.unselect()
mail.ImapClient.unselect() -> ImapClient
Closes the open mailbox without removing anything, which is what CLOSE
cannot do.
Returns ImapClient — itself.
Raises StateError if the server does not offer UNSELECT.
ImapClient.list()
mail.ImapClient.list(reference: ?string, pattern: ?string) -> list
The mailboxes matching a pattern.
* matches anything including the hierarchy separator, and % matches
anything except it, which is what lists one level.
for box in inbox.list('', '*') {
echo box.name
}
Parameters
reference(?string) — The prefix to search under. Empty when not given.pattern(?string) —*when not given.
Returns list — of MailboxInfo
ImapClient.lsub()
mail.ImapClient.lsub(reference: ?string, pattern: ?string) -> list
The mailboxes matching a pattern that this account is subscribed to.
Parameters
reference(?string)pattern(?string)
Returns list — of MailboxInfo
ImapClient.status()
mail.ImapClient.status(name: string, items: ?list) -> dict
What is in a mailbox without opening it.
echo inbox.status('INBOX', ['MESSAGES', 'UNSEEN'])
Parameters
name(string)items(?list) —MESSAGES,RECENT,UIDNEXT,UIDVALIDITYandUNSEEN. All five when not given.
Returns dict — of the items asked for, by name.
ImapClient.create()
mail.ImapClient.create(name: string) -> ImapClient
Creates a mailbox.
Parameters
name(string)
Returns ImapClient — itself.
Raises ImapError if it exists already or the name is not allowed.
ImapClient.delete()
mail.ImapClient.delete(name: string) -> ImapClient
Deletes a mailbox and everything in it.
Parameters
name(string)
Returns ImapClient — itself.
Raises ImapError if there is no such mailbox.
ImapClient.rename()
mail.ImapClient.rename(from: string, to: string) -> ImapClient
Renames a mailbox.
Parameters
from(string)to(string)
Returns ImapClient — itself.
ImapClient.subscribe()
mail.ImapClient.subscribe(name: string) -> ImapClient
Marks a mailbox as one this account wants to see.
Parameters
name(string)
Returns ImapClient — itself.
ImapClient.unsubscribe()
mail.ImapClient.unsubscribe(name: string) -> ImapClient
Undoes subscribe().
Parameters
name(string)
Returns ImapClient — itself.
ImapClient.search()
mail.ImapClient.search(criteria: string, by_uid: ?bool) -> list
The messages in the open mailbox matching a search.
The criteria are IMAP’s own, which read close to English: 'UNSEEN',
'FROM ann@example.com', 'SINCE 1-Jan-2026 SMALLER 10000'.
for uid in inbox.search('UNSEEN SINCE 1-Jan-2026', true) {
echo uid
}
Parameters
criteria(string)by_uid(?bool) — Return unique identifiers rather than positions, which is what anything that outlives the connection wants.
Returns list — of number
Raises StateError if no mailbox is open.
ImapClient.fetch()
mail.ImapClient.fetch(ids, items: ?string, by_uid: ?bool) -> list
Fetches what the server knows about some messages.
for info in inbox.fetch('1:10', 'ENVELOPE FLAGS', false) {
echo '${info.sequence} ${info.envelope.subject}'
}
Parameters
ids(string|list|number) — A set, as'1:10'or'1,3,5', or a list of numbers, or one number.items(?string) — What to fetch, in IMAP’s own words.ENVELOPE FLAGS INTERNALDATE RFC822.SIZEwhen not given.by_uid(?bool) — Whetheridsare unique identifiers.
Returns list — of MessageInfo
Raises StateError if no mailbox is open.
ImapClient.fetch_message()
mail.ImapClient.fetch_message(id: number, by_uid: ?bool, mark_seen: ?bool) -> Message
Fetches one message whole.
Reading a message this way does not mark it read, because a program that
goes through a mailbox should not change what a person sees when they
next open it. Pass mark_seen to say otherwise, which is what a mail
client showing a message wants.
echo inbox.fetch_message(uid, true, false).subject()
Parameters
id(number)by_uid(?bool)mark_seen(?bool) — Whether to set\Seenon the way.
Returns Message
Raises ImapError if there is no such message.
ImapClient.fetch_headers()
mail.ImapClient.fetch_headers(ids, by_uid: ?bool) -> list
Fetches only the headers of some messages, which is enough to show a list without pulling the bodies across.
Parameters
ids(string|list|number)by_uid(?bool)
Returns list — of MessageInfo, each with parts holding the
header block.
ImapClient.store()
mail.ImapClient.store(ids, action: string, flags: list, by_uid: ?bool) -> list
Changes the flags on some messages.
Parameters
ids(string|list|number)action(string) —FLAGSto replace,+FLAGSto add,-FLAGSto remove. Add.SILENTto any of them to stop the server reporting the result back.flags(list) — The flags, as['\\Seen'].by_uid(?bool)
Returns list — of MessageInfo describing what changed.
ImapClient.add_flags()
mail.ImapClient.add_flags(ids, flags: list, by_uid: ?bool) -> list
Adds flags to some messages, leaving the rest alone.
Parameters
ids(string|list|number)flags(list)by_uid(?bool)
Returns list — of MessageInfo
ImapClient.remove_flags()
mail.ImapClient.remove_flags(ids, flags: list, by_uid: ?bool) -> list
Removes flags from some messages.
Parameters
ids(string|list|number)flags(list)by_uid(?bool)
Returns list — of MessageInfo
ImapClient.mark_seen()
mail.ImapClient.mark_seen(ids, by_uid: ?bool) -> list
Marks messages read.
Parameters
ids(string|list|number)by_uid(?bool)
Returns list — of MessageInfo
ImapClient.mark_deleted()
mail.ImapClient.mark_deleted(ids, by_uid: ?bool) -> list
Marks messages for removal. Nothing goes until expunge().
Parameters
ids(string|list|number)by_uid(?bool)
Returns list — of MessageInfo
ImapClient.copy()
mail.ImapClient.copy(ids, target: string, by_uid: ?bool) -> ImapClient
Copies messages into another mailbox.
Parameters
ids(string|list|number)target(string)by_uid(?bool)
Returns ImapClient — itself.
ImapClient.move()
mail.ImapClient.move(ids, target: string, by_uid: ?bool) -> ImapClient
Moves messages into another mailbox.
Uses the server’s own MOVE where there is one, and otherwise does what
MOVE was invented to replace: copy, mark deleted, expunge.
Parameters
ids(string|list|number)target(string)by_uid(?bool)
Returns ImapClient — itself.
ImapClient.expunge()
mail.ImapClient.expunge() -> list
Removes every message marked deleted in the open mailbox.
Returns list — of number: the positions that went, highest first,
which is the order they have to be applied in.
ImapClient.append()
mail.ImapClient.append(mailbox: string, note, flags: ?list, received) -> ImapClient
Adds a message to a mailbox without sending it anywhere, which is how a sent message gets into the Sent folder.
Parameters
mailbox(string)note(Message|string|bytes)flags(?list) —['\\Seen']is the usual one for a sent message.received(?Date) — The internal date to file it under. The time of arrival when not given.
Returns ImapClient — itself.
ImapClient.idle()
mail.ImapClient.idle(timeout: ?number) -> list
Waits for the server to say something, which is how a program learns about new mail without asking over and over.
Returns when the server reports anything, or when the wait runs out. A server may drop a connection that idles for too long, so even a program with nothing else to do should come back around every twenty minutes or so; RFC 2177 says as much.
inbox.on_event(@(response) {
echo 'server said ${response.name()}'
})
while true {
inbox.idle(1500000)
}
Parameters
timeout(?number) — Milliseconds to wait.1500000, which is twenty-five minutes, when not given.
Returns list — of the untagged responses that arrived.
Raises StateError if the server does not offer IDLE.
ImapClient.noop()
mail.ImapClient.noop() -> list
Asks the server for nothing, which both keeps the connection alive and gives it a chance to report anything that has changed.
Returns list — of the untagged responses that came with it.
ImapClient.namespace()
mail.ImapClient.namespace()
The namespaces this account has: its own, other people’s, and the shared ones.
Returns — list, as the server sent it.
Raises StateError if the server does not offer NAMESPACE.
ImapClient.enable()
mail.ImapClient.enable(names: list) -> ImapClient
Turns on an extension the server offers but does not use until asked,
such as UTF8=ACCEPT.
Parameters
names(list)
Returns ImapClient — itself.
Raises StateError if the server does not offer ENABLE.
ImapClient.id()
mail.ImapClient.id(fields: ?dict) -> dict
Tells the server what this client is, and reads back what the server is. Both sides may say nothing at all.
Parameters
fields(?dict) —name,versionand whatever else is worth saying. A name ofZuriwhen not given.
Returns dict — of what the server said about itself.
ImapClient.logout()
mail.ImapClient.logout() -> ImapClient
Says goodbye and closes the connection.
Returns ImapClient — itself.
ImapClient.close()
mail.ImapClient.close() -> ImapClient
Closes the connection without saying goodbye.
Returns ImapClient — itself.
ImapClient.to_string()
mail.ImapClient.to_string()
2026, Richard Ore and Zuri contributors
mail.imap.parser
import mail.imap.parser
The IMAP grammar: turning what a server says into something a program can read.
IMAP does not speak in lines alone. A response may carry a literal, which is a byte count followed by exactly that many bytes of anything at all, including line breaks, so reading one response can mean reading several lines and a block of raw data. That is why this reads from the connection rather than from a string.
import mail.imap.parser
var tokens = parser.tokenize('(\\Seen \\Answered) "/" "INBOX"')
echo tokens[0]
echo tokens[2]
[\Seen, \Answered]
INBOX
Constants
STATUSES
mail.parser.STATUSES = [...]
SYSTEM_FLAGS
mail.parser.SYSTEM_FLAGS = [...]
Functions
tokenize()
mail.parser.tokenize(text: string, source) -> list
Reads a run of IMAP values out of text.
source is only needed when the text can contain a literal, which is to
say whenever it came from a server rather than from a test.
Parameters
text(string)source(?LineStream) — Where to read a literal’s bytes from.
Returns list
Raises ProtocolError if the text is not well formed.
quote()
mail.parser.quote(value: string) -> string|Literal
Writes a value the way a command has to carry it.
A name that is plain enough goes as it is, one that is not is quoted, and one that cannot be quoted at all, because it holds a line break or characters outside ASCII, is sent as a literal.
Parameters
value(string)
Returns string|Literal
Classes
Literal
class mail.parser.Literal
A run of bytes a server sent as a literal.
Kept apart from an ordinary string because a literal is where a message body arrives, and a body is bytes: decoding it as text would corrupt anything that is not text.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.parser.Literal(data)
Parameters
data(bytes)
Literal.to_text()
mail.parser.Literal.to_text() -> string
The literal read as UTF-8 text.
Returns string
Literal.length()
mail.parser.Literal.length() -> number
How many bytes it holds.
Returns number
Literal.to_string()
mail.parser.Literal.to_string()
Response
class mail.parser.Response
One response from the server.
kind is tagged for the answer to a command, untagged for
everything a server says on its own, and continuation for the
invitation to send the rest of a command.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.parser.Response(kind: string, tag: ?string, tokens: list, line: string)
Parameters
kind(string)tag(?string)tokens(list) — The values in the response, in order.line(string) — The first line it was read from.
Response.is_ok()
mail.parser.Response.is_ok() -> bool
Whether this is a command’s answer and the answer was yes.
Returns bool
Response.name()
mail.parser.Response.name() -> string|nil
The name of an untagged data response, such as EXISTS, FETCH or
LIST, or nil when it is not one.
A numbered response puts the number first, so the name is the second value in those and the first in the rest.
Returns string|nil
Response.number()
mail.parser.Response.number() -> number|nil
The number a numbered response carries, such as the message number in * 12 FETCH,
or nil when it carries none.
Returns number|nil
Response.arguments()
mail.parser.Response.arguments() -> list
The values after the response’s name.
Returns list
Response.to_string()
mail.parser.Response.to_string()
Reader
class mail.parser.Reader
Reads responses off a connection, one at a time.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.parser.Reader(source)
Parameters
source(LineStream)
Reader.next()
mail.parser.Reader.next() -> Response
Reads the next response, however many lines and literals it takes.
Returns Response
Raises ProtocolError if what arrives is not a response.
Raises ConnectionClosed if the connection ends first.
Reader.to_string()
mail.parser.Reader.to_string()
2026, Richard Ore and Zuri contributors
mail.imap.server
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.imap.serveron its own works too and reaches the same definitions.
The serving end of IMAP.
ImapServer accepts connections, runs the session state machine, and
answers every command out of a MailStore. What mail there is and where
it lives is the store’s business; this knows only the protocol.
import mail.imap { ImapServer, MaildirStore }
var store = MaildirStore('/var/mail')
store.add_account('ann', secret)
ImapServer({ port: 143 }, store).listen()
Constants
BASE_CAPABILITIES
mail.BASE_CAPABILITIES = [...]
ALLOWED_FLAGS
mail.ALLOWED_FLAGS = [...]
DEFAULT_TIMEOUT
mail.DEFAULT_TIMEOUT = 1800000
Classes
ImapSession
class mail.ImapSession
One connection, and what it has got as far as doing.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.ImapSession(peer: string, secure: bool)
Parameters
peer(string)secure(bool)
ImapSession.is_selected()
mail.ImapSession.is_selected() -> bool
Whether a mailbox is open.
Returns bool
ImapSession.to_string()
mail.ImapSession.to_string()
ImapServer
class mail.ImapServer
A server that serves mail.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.ImapServer(options: ?dict, store)
| option | default | what it does |
|---|---|---|
host | 127.0.0.1 | the address to bind |
port | 143 | the port to bind |
greeting | Zuri IMAP ready | what to say on connecting |
timeout | 1800000 | milliseconds a client may go quiet |
require_tls | false | refuse to authenticate without TLS |
Parameters
options(?dict)store(MailStore) — Where the mail is.
ImapServer.on_connect()
mail.ImapServer.on_connect(handler) -> ImapServer
Called with the session when a connection opens.
Parameters
handler(function(1))
Returns ImapServer — itself.
ImapServer.on_close()
mail.ImapServer.on_close(handler) -> ImapServer
Called with the session when a connection closes.
Parameters
handler(function(1))
Returns ImapServer — itself.
ImapServer.on_error()
mail.ImapServer.on_error(handler) -> ImapServer
Called with the error and the session when something inside the server fails.
Parameters
handler(function(2))
Returns ImapServer — itself.
ImapServer.use_tls()
mail.ImapServer.use_tls(cert_chain: string, private_key: string) -> ImapServer
Gives the server a certificate, which is what lets it offer STARTTLS.
Parameters
cert_chain(string)private_key(string)
Returns ImapServer — itself.
ImapServer.set_tls_config()
mail.ImapServer.set_tls_config(config) -> ImapServer
Gives the server a TLS configuration built elsewhere.
Parameters
config(TlsConfig)
Returns ImapServer — itself.
ImapServer.bind()
mail.ImapServer.bind() -> ImapServer
Binds the listening socket without accepting anything yet.
Returns ImapServer — itself.
ImapServer.address()
mail.ImapServer.address() -> SocketAddr
The address the server is listening on.
Returns SocketAddr
ImapServer.accept()
mail.ImapServer.accept() -> ImapServer
Accepts one connection and serves it to the end.
Returns ImapServer — itself.
ImapServer.listen()
mail.ImapServer.listen() -> ImapServer
Accepts connections one after another until close().
One at a time: an IMAP connection is long-lived, so a server expecting more than one client at once wants a worker per connection.
Returns ImapServer — itself.
ImapServer.close()
mail.ImapServer.close() -> ImapServer
Stops the accept loop and closes the listening socket.
Returns ImapServer — itself.
ImapServer.is_listening()
mail.ImapServer.is_listening() -> bool
Whether the accept loop is running.
Returns bool
ImapServer.serve_connection()
mail.ImapServer.serve_connection(client) -> ImapServer
Serves one already-accepted connection to the end.
Parameters
client(TcpStream)
Returns ImapServer — itself.
ImapServer.to_string()
mail.ImapServer.to_string()
2026, Richard Ore and Zuri contributors
mail.imap.store
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.imap.storeon its own works too and reaches the same definitions.
Where an ImapServer keeps mail.
MailStore is the contract: what a store has to be able to do for the
server to answer with it. Two are provided. MaildirStore keeps mail on
disk in the Maildir layout, which every other mail tool can read, so a
mailbox written here is not trapped here. MemoryStore keeps it in the
process and forgets it on exit, which is what a test and an embedded
server want.
import mail.imap { ImapServer, MaildirStore }
var store = MaildirStore('/var/mail/ann')
store.add_account('ann', 'secret')
ImapServer({ port: 143 }, store).listen()
A store of your own only has to answer the same calls. Nothing here is required to come from a file.
Constants
MAILDIR_PARTS
mail.MAILDIR_PARTS = [...]
INFO_SEPARATOR
mail.INFO_SEPARATOR
MAILDIR_FLAGS
mail.MAILDIR_FLAGS = {...}
DELIMITER
mail.DELIMITER = '.'
INDEX_FILE
mail.INDEX_FILE = 'zuri-uidlist'
Classes
StoredMessage
class mail.StoredMessage
One message in a store.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.StoredMessage(uid: number, raw, flags: list, received)
Parameters
uid(number) — Unique within its mailbox, and never reused.raw(bytes) — The message exactly as it arrived.flags(list)received(Date) — When the store took it.
StoredMessage.size()
mail.StoredMessage.size() -> number
How large the message is.
Returns number
StoredMessage.has_flag()
mail.StoredMessage.has_flag(flag: string) -> bool
Whether a flag is set.
Parameters
flag(string)
Returns bool
StoredMessage.to_string()
mail.StoredMessage.to_string()
MailStore
class mail.MailStore
What a store has to be able to do.
Every method raises here; a store is something that answers them. Both of the ones that ship do, and so can one of your own.
- printable — has a
@to_string(), soechoandprint()show something useful
MailStore.authenticate()
mail.MailStore.authenticate(username: string, password: string) -> bool
Whether these credentials name an account.
Parameters
username(string)password(string)
Returns bool
MailStore.password_of()
mail.MailStore.password_of(username: string) -> string|nil
The password of an account, for the mechanisms that prove it without
sending it, or nil when there is no such account or the store cannot
produce one.
A store that keeps only hashed passwords returns nil, and the server
then does not offer those mechanisms.
Parameters
username(string)
Returns string|nil
MailStore.has_passwords()
mail.MailStore.has_passwords() -> bool
Whether this store can produce a password rather than only check one.
A server offers the mechanisms that prove a password without sending it only when it can work out the same proof, which means knowing the password. A store that keeps only hashes, or that checks credentials somewhere else entirely, says no here and the server stops offering them.
Returns bool
MailStore.mailboxes()
mail.MailStore.mailboxes(username: string) -> list
The mailboxes an account has.
Parameters
username(string)
Returns list — of string
MailStore.exists()
mail.MailStore.exists(username: string, mailbox: string) -> bool
Whether a mailbox exists.
Parameters
username(string)mailbox(string)
Returns bool
MailStore.create()
mail.MailStore.create(username: string, mailbox: string)
Creates a mailbox.
Parameters
username(string)mailbox(string)
Raises MailboxError if it exists already.
MailStore.remove()
mail.MailStore.remove(username: string, mailbox: string)
Removes a mailbox and everything in it.
Parameters
username(string)mailbox(string)
Raises MailboxError if there is no such mailbox.
MailStore.rename()
mail.MailStore.rename(username: string, from: string, to: string)
Renames a mailbox.
Parameters
username(string)from(string)to(string)
Raises MailboxError if there is no such mailbox, or the new name
is taken.
MailStore.messages()
mail.MailStore.messages(username: string, mailbox: string) -> list
Every message in a mailbox, oldest first.
Parameters
username(string)mailbox(string)
Returns list — of StoredMessage
Raises MailboxError if there is no such mailbox.
MailStore.append()
mail.MailStore.append(username: string, mailbox: string, raw, flags: ?list, received) -> StoredMessage
Adds a message to a mailbox.
Parameters
username(string)mailbox(string)raw(bytes)flags(?list)received(?Date)
Returns StoredMessage
Raises MailboxError if there is no such mailbox.
MailStore.set_flags()
mail.MailStore.set_flags(username: string, mailbox: string, uid: number, flags: list)
Replaces the flags on one message.
Parameters
username(string)mailbox(string)uid(number)flags(list)
Raises MailboxError if there is no such message.
MailStore.expunge()
mail.MailStore.expunge(username: string, mailbox: string) -> list
Removes every message in a mailbox marked \Deleted.
Parameters
username(string)mailbox(string)
Returns list — of number: the identifiers that went.
MailStore.counters()
mail.MailStore.counters(username: string, mailbox: string) -> dict
What the next message added to a mailbox will be numbered, and how to tell that the numbering has been reset.
Parameters
username(string)mailbox(string)
Returns dict — of uidnext and uidvalidity.
MailStore.to_string()
mail.MailStore.to_string()
MemoryStore
class mail.MemoryStore < MailStore
A store that keeps everything in the process and nothing on disk.
Fast, and gone when the program is. This is what a test wants, and what a server embedded in something else wants when the mail it holds is not the point.
import mail.imap { MemoryStore }
var store = MemoryStore()
store.add_account('ann', 'secret')
store.append('ann', 'INBOX', 'From: a@b.com\r\n\r\nhello', nil, nil)
echo store.mailboxes('ann')
echo store.messages('ann', 'INBOX').length()
[INBOX]
1
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.MemoryStore(mailboxes: ?list)
Parameters
mailboxes(?list) — The mailboxes every new account starts with.['INBOX']when not given.
MemoryStore.add_account()
mail.MemoryStore.add_account(username: string, password: string) -> MemoryStore
Adds an account, with the mailboxes a new account starts with.
Parameters
username(string)password(string)
Returns MemoryStore — itself.
MemoryStore.authenticate()
mail.MemoryStore.authenticate(username: string, password: string)
MemoryStore.password_of()
mail.MemoryStore.password_of(username: string)
MemoryStore.has_passwords()
mail.MemoryStore.has_passwords()
MemoryStore.mailboxes()
mail.MemoryStore.mailboxes(username: string)
MemoryStore.exists()
mail.MemoryStore.exists(username: string, mailbox: string)
MemoryStore.create()
mail.MemoryStore.create(username: string, mailbox: string)
MemoryStore.remove()
mail.MemoryStore.remove(username: string, mailbox: string)
MemoryStore.rename()
mail.MemoryStore.rename(username: string, from: string, to: string)
MemoryStore.messages()
mail.MemoryStore.messages(username: string, mailbox: string)
MemoryStore.append()
mail.MemoryStore.append(username: string, mailbox: string, raw, flags: ?list, received)
MemoryStore.set_flags()
mail.MemoryStore.set_flags(username: string, mailbox: string, uid: number, flags: list)
MemoryStore.expunge()
mail.MemoryStore.expunge(username: string, mailbox: string)
MemoryStore.counters()
mail.MemoryStore.counters(username: string, mailbox: string)
MemoryStore.to_string()
mail.MemoryStore.to_string()
MaildirStore
class mail.MaildirStore < MailStore
A store that keeps mail on disk in the Maildir layout.
Each account is a directory under root, holding its INBOX directly and
every other mailbox as .Name beside it, which is the Maildir++
arrangement every other mail tool understands. A mailbox written here
can be read by anything else, and mail delivered by anything else turns
up here.
Flags are kept the Maildir way, as letters at the end of a message’s
filename after :2,. A colon cannot appear in a Windows filename, so on
Windows the flags follow ;2, instead, which is what mbsync writes
there too. A Maildir moved between Windows and any other system keeps
its messages but has to have their flags renamed to match.
Mail is on disk; accounts are not. The store asks the program who its
users are, either as a password given to add_account() or through
set_authenticator(), because where an application keeps its passwords
is the application’s business and not a mail library’s.
import mail.imap { MaildirStore }
var store = MaildirStore('/var/mail')
store.add_account('ann', secret)
store.append('ann', 'INBOX', raw, nil, nil)
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.MaildirStore(root: string)
Parameters
root(string) — The directory the accounts live under. It is created if it is not there.
MaildirStore.add_account()
mail.MaildirStore.add_account(username: string, password: string) -> MaildirStore
Adds an account with a password this store will check itself, and makes its mail directory if it is not there.
Parameters
username(string)password(string)
Returns MaildirStore — itself.
MaildirStore.set_authenticator()
mail.MaildirStore.set_authenticator(checker) -> MaildirStore
Hands credential checking to a function of your own, which is what a program with its own user table wants.
A store with an authenticator can no longer produce a password, so the server stops offering the mechanisms that need one.
Parameters
checker(function(2)) — Given a username and a password, returns whether they are right.
Returns MaildirStore — itself.
MaildirStore.authenticate()
mail.MaildirStore.authenticate(username: string, password: string)
MaildirStore.password_of()
mail.MaildirStore.password_of(username: string)
MaildirStore.has_passwords()
mail.MaildirStore.has_passwords()
MaildirStore.mailboxes()
mail.MaildirStore.mailboxes(username: string)
MaildirStore.exists()
mail.MaildirStore.exists(username: string, mailbox: string)
MaildirStore.create()
mail.MaildirStore.create(username: string, mailbox: string)
MaildirStore.remove()
mail.MaildirStore.remove(username: string, mailbox: string)
MaildirStore.rename()
mail.MaildirStore.rename(username: string, from: string, to: string)
MaildirStore.messages()
mail.MaildirStore.messages(username: string, mailbox: string)
MaildirStore.append()
mail.MaildirStore.append(username: string, mailbox: string, raw, flags: ?list, received)
MaildirStore.set_flags()
mail.MaildirStore.set_flags(username: string, mailbox: string, uid: number, flags: list)
MaildirStore.expunge()
mail.MaildirStore.expunge(username: string, mailbox: string)
MaildirStore.counters()
mail.MaildirStore.counters(username: string, mailbox: string)
MaildirStore.to_string()
mail.MaildirStore.to_string()
2026, Richard Ore and Zuri contributors
mail.message
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.messageon its own works too and reaches the same definitions.
A mail message: its headers, its body, and the tree of parts a body turns into once there is more than one thing in it.
Message is both what comes off the wire and what goes onto it. Parsing
one gives the whole MIME tree with every part decoded on demand;
building one from text, HTML and files gives back a message any mail
server will accept.
import mail
var note = mail.message()
.set_from('Richard Ore <richard@example.com>')
.add_to('ann@example.com')
.set_subject('Quarterly report')
.set_text('The numbers are attached.')
echo note.subject()
echo note.content_type().mime_type()
Quarterly report
text/plain
A message with both text and HTML in it becomes a
multipart/alternative; add a file and the whole thing is wrapped in a
multipart/mixed. None of that has to be spelled out: the shape follows
from what was put in.
Functions
text_part()
mail.text_part(text: string, charset: ?string, subtype: ?string) -> Message
Builds one text part.
Parameters
text(string)charset(?string) —utf-8, orus-asciiwhen the text needs nothing more. No other set can be written.subtype(?string) —plainwhen not given.
Returns Message
Raises MessageError if charset names a set this cannot write.
attachment()
mail.attachment(data) -> Attachment
Starts an attachment from what is in it.
import mail
echo mail.attachment('some bytes').set_filename('notes.txt').to_string()
<Attachment notes.txt, 10 bytes>
Parameters
data(string|bytes)
Returns Attachment
multipart()
mail.multipart(subtype: string, parts: list)
new_message_id()
mail.new_message_id(domain: ?string) -> string
An identifier for a message, unique enough that no two ever collide.
Parameters
domain(?string) — The right hand side. The machine’s own name when not given.
Returns string — including the angle brackets a header needs.
parse()
mail.parse(data) -> Message
Reads a message.
Parameters
data(string|bytes)
Returns Message
message()
mail.message() -> Message
Starts a message.
What goes in it is said one call at a time, and every call hands the message back so they read as one:
import mail
var note = mail.message()
.set_from('Richard Ore <richard@example.com>')
.add_to('ann@example.com')
.set_subject('Quarterly report')
.set_text('The numbers are in.')
echo note.subject()
echo note.content_type().mime_type()
Quarterly report
text/plain
The shape follows from what is said. Text alone is a text/plain
message; adding HTML makes it a multipart/alternative; attaching a
file wraps the whole thing in a multipart/mixed. None of that has to
be arranged by hand.
A message starts dated, since the time it was written is the time it was written. It gets its identifier when it is sent, because that is when the domain it belongs to is settled.
Returns Message
Classes
Message
class mail.Message
One message, or one part of one.
A part is a message too: it has headers and a body, and its body may be more parts. The same class covers both, so walking a message and reading a standalone one are the same code.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.Message(headers, source)
Builds a message.
Most callers want mail.message(), which fills one in, or
mail.parse(), which reads one off the wire.
Parameters
headers(?Headers) — The headers to start with.source(?any) — Bytes or text to read the message from, in which caseheadersis ignored.
Message.parse()
mail.Message.parse(data) -> Message
Reads a message.
Nothing is decoded up front. The tree is built, and each part decodes its own body the first time something asks for it, so reading the subject of a message with a large attachment in it costs nothing more than reading the headers.
import mail
var note = mail.parse(
'From: a@example.com\r\n'
+ 'Subject: Hi\r\n'
+ '\r\n'
+ 'Hello there.\r\n'
)
echo note.subject()
echo note.text()
Hi
Hello there.
Parameters
data(string|bytes)
Returns Message
Raises MessageError if the headers cannot be read.
Message.content_type()
mail.Message.content_type() -> ContentType
The Content-Type of this part.
Returns ContentType
Message.is_multipart()
mail.Message.is_multipart() -> bool
Whether this part holds other parts.
Returns bool
Message.parts()
mail.Message.parts() -> list
The parts directly inside this one, which is empty for a part that holds content rather than other parts.
Returns list — of Message
Message.walk()
mail.Message.walk() -> list
Every part of the message including this one, outermost first.
for part in note.walk() {
echo part.content_type().mime_type()
}
Returns list — of Message
Message.body_bytes()
mail.Message.body_bytes() -> bytes
This part’s body, decoded from whatever transfer encoding it arrived in.
A part that holds other parts has no body of its own and gives back nothing.
Returns bytes
Message.text()
mail.Message.text() -> string
This part’s body as text, decoded from its transfer encoding and then from its character set.
Returns string
Message.raw_body()
mail.Message.raw_body() -> bytes
The body still encoded, exactly as it sits in the message.
Returns bytes
Message.set_raw_body()
mail.Message.set_raw_body(data) -> Message
Replaces the body with bytes that are already in the transfer encoding the headers name, and drops any parts this held.
The encoding is not applied here and not checked. set_text() and
attach() are the calls that do both.
Parameters
data(string|bytes)
Returns Message — itself.
Message.set_parts()
mail.Message.set_parts(parts: list) -> Message
Replaces the parts this one holds.
Parameters
parts(list) —Messagevalues.
Returns Message — itself.
Message.is_empty()
mail.Message.is_empty() -> bool
Whether this part holds nothing at all: no body and no parts.
Returns bool
Message.source_headers()
mail.Message.source_headers() -> string|nil
The bytes this part’s headers were read from, or nil for a part that
was built rather than parsed.
A signature covers the message as it was written, not as it would be written again, which is what this is for.
Returns string|nil
Message.source_body()
mail.Message.source_body() -> bytes|nil
The bytes this part’s body was read from, or nil for a part that was
built rather than parsed.
Returns bytes|nil
Message.subject()
mail.Message.subject() -> string
The subject, with any encoded words in it decoded.
Returns string — empty when the message has no subject.
Message.set_subject()
mail.Message.set_subject(text: string) -> Message
Sets the subject, encoding it if it holds characters a header cannot carry.
Parameters
text(string)
Returns Message — itself.
Message.sender()
mail.Message.sender() -> Address|nil
The sender, or nil when the message has no From.
Returns Address|nil
Message.to()
mail.Message.to() -> list
The addresses in To.
Returns list — of Address
Message.cc()
mail.Message.cc() -> list
The addresses in Cc.
Returns list — of Address
Message.bcc()
mail.Message.bcc() -> list
The addresses in Bcc.
A message that has been sent should not have this header at all, since
the whole point of a blind copy is that the other recipients cannot see
it. mail.send() strips it before handing the message over, after
taking the recipients from it.
Returns list — of Address
Message.reply_to()
mail.Message.reply_to() -> list
The addresses in Reply-To, falling back to the sender when there is no
such header, which is what a mail client does.
Returns list — of Address
Message.recipients()
mail.Message.recipients() -> list
Everyone the message is addressed to: To, Cc and Bcc together,
with duplicates removed.
Returns list — of Address
Message.sent_at()
mail.Message.sent_at() -> Date|nil
The Date header as a date, or nil when it is absent or unreadable.
Returns Date|nil
Message.message_id()
mail.Message.message_id() -> string|nil
The Message-ID, with the angle brackets left on, or nil.
Returns string|nil
Message.references()
mail.Message.references() -> list
The message identifiers in References, oldest first, which is the
chain of replies this message belongs to.
Returns list — of string
Message.filename()
mail.Message.filename() -> string|nil
The name this part suggests being saved under, from its disposition or,
failing that, its type. nil when it suggests none.
Returns string|nil
Message.is_attachment()
mail.Message.is_attachment() -> bool
Whether this part is meant to be offered as a file rather than shown in place.
A part is an attachment when it says so, and also when it says nothing but carries a filename, which is what older mailers do.
Returns bool
Message.find_part()
mail.Message.find_part(mime_type: string) -> Message|nil
The first part of a given type anywhere in the message, or nil.
Parameters
mime_type(string) — Astext/html.
Returns Message|nil
Message.text_body()
mail.Message.text_body() -> string|nil
The plain text of the message, wherever in the tree it is, or nil when
there is none.
Returns string|nil
Message.html_body()
mail.Message.html_body() -> string|nil
The HTML of the message, wherever in the tree it is, or nil when there
is none.
Returns string|nil
Message.attachments()
mail.Message.attachments() -> list
Every part of the message that is an attachment.
Returns list — of Message
Message.take_content()
mail.Message.take_content() -> Message
Moves this part’s content into a part of its own and hands it back, leaving this one holding the message-level headers and nothing else.
This is what turns a message into the first child of the multipart that
is about to replace it, which is why attach() can wrap a message
without the caller rebuilding it.
Returns Message
Message.become_multipart()
mail.Message.become_multipart(subtype: string, parts: list) -> Message
Turns this part into a multipart holding the given parts, under a boundary nothing inside it can contain.
Parameters
subtype(string) —mixed,alternative,relatedordigest.parts(list) — TheMessagevalues to put in it.
Returns Message — itself.
Message.set_from()
mail.Message.set_from(person) -> Message
Sets who the message is from, replacing whoever was there.
note.set_from('Richard Ore <richard@example.com>')
Parameters
person(string|Address)
Returns Message — itself.
Message.set_sender()
mail.Message.set_sender(person) -> Message
Sets the Sender header, which says who actually put the message on the
wire when that is not who it is from.
Parameters
person(string|Address)
Returns Message — itself.
Message.add_to()
mail.Message.add_to(people) -> Message
Adds recipients, keeping any already there.
note.add_to('ann@example.com').add_to('bob@example.com')
Parameters
people(string|Address|list)
Returns Message — itself.
Message.add_cc()
mail.Message.add_cc(people) -> Message
Adds copied recipients, keeping any already there.
Parameters
people(string|Address|list)
Returns Message — itself.
Message.add_bcc()
mail.Message.add_bcc(people) -> Message
Adds blind copied recipients.
The header is removed before the message is handed to a server, after the recipients have been taken from it, which is the whole point of a blind copy.
Parameters
people(string|Address|list)
Returns Message — itself.
Message.set_to()
mail.Message.set_to(people) -> Message
Sets the recipients, replacing any already there.
Parameters
people(string|Address|list)
Returns Message — itself.
Message.set_cc()
mail.Message.set_cc(people) -> Message
Sets the copied recipients, replacing any already there.
Parameters
people(string|Address|list)
Returns Message — itself.
Message.set_bcc()
mail.Message.set_bcc(people) -> Message
Sets the blind copied recipients, replacing any already there.
Parameters
people(string|Address|list)
Returns Message — itself.
Message.set_reply_to()
mail.Message.set_reply_to(people) -> Message
Sets where replies should go, when that is not the sender.
Parameters
people(string|Address|list)
Returns Message — itself.
Message.set_header()
mail.Message.set_header(name: string, value) -> Message
Sets any header at all, replacing one already there.
The value is written exactly as given, so a header that needs encoding
needs it applied first. set_subject() and the address setters do that
for the headers where it matters.
Parameters
name(string)value(any)
Returns Message — itself.
Message.add_header()
mail.Message.add_header(name: string, value) -> Message
Adds a header, keeping any already there. Received is the header this
is for.
Parameters
name(string)value(any)
Returns Message — itself.
Message.set_date()
mail.Message.set_date(moment) -> Message
Sets when the message was written.
A message built by mail.message() is dated for you, so this is for
saying otherwise.
Parameters
moment(?Date) — The current time when not given.
Returns Message — itself.
Message.set_message_id()
mail.Message.set_message_id(id: ?string) -> Message
Sets the message’s own identifier.
Parameters
id(?string) — A fresh one, built from the sender’s domain, when not given.
Returns Message — itself.
Message.set_in_reply_to()
mail.Message.set_in_reply_to(id: string) -> Message
Marks the message as a reply to another, which is what puts the two in the same conversation in a mail client.
reply.set_in_reply_to(original.message_id())
.set_references(original.references() + [original.message_id()])
Parameters
id(string) — The identifier of the message being answered.
Returns Message — itself.
Message.set_references()
mail.Message.set_references(ids) -> Message
Sets the chain of identifiers this message belongs to, oldest first.
Parameters
ids(list|string)
Returns Message — itself.
Message.set_text()
mail.Message.set_text(text: string, charset: ?string) -> Message
Sets the plain text of the message.
Replaces the text that is already there, wherever in the tree it is. A
message that has HTML but no text becomes a multipart/alternative
holding both.
Parameters
text(string)charset(?string) —utf-8when not given.
Returns Message — itself.
Message.set_html()
mail.Message.set_html(html: string, charset: ?string) -> Message
Sets the HTML of the message.
Parameters
html(string)charset(?string) —utf-8when not given.
Returns Message — itself.
Message.attach()
mail.Message.attach(part) -> Message
Adds an attachment to the message.
The message becomes a multipart/mixed if it is not one already, with
whatever it held before as the first part. That happens once, however
many attachments are added.
note.attach(mail.attachment(figures).set_filename('figures.csv'))
note.attach(mail.Attachment.from_file('reports/q3.pdf'))
Parameters
part(Attachment|Message) — An attachment, or a part that is already built.
Returns Message — itself.
Raises MessageError if handed anything else.
Message.embed()
mail.Message.embed(part) -> string
Adds something to be shown inside the HTML rather than offered
separately, and returns the reference to point an <img> at.
The message becomes a multipart/related around the content it already
held, which is what tells a reader the two belong together.
var source = note.embed(mail.attachment(logo).set_filename('logo.png'))
note.set_html('<img src="${source}">')
Parameters
part(Attachment|Message)
Returns string — the cid: URL that refers to it.
Raises MessageError if handed anything else.
Message.to_bytes()
mail.Message.to_bytes() -> bytes
The whole message, ready to be written to a socket or a file.
Lines end with a carriage return and a newline, which is what the protocols require whatever the local convention is.
Returns bytes
Message.to_string()
mail.Message.to_string() -> string
The whole message as text.
Returns string
Attachment
class mail.Attachment
Something a message carries alongside what it says: a file to offer, or an image the HTML shows.
Built one call at a time, the way a message is, and handed to
Message.attach() or Message.embed().
import mail
var figures = mail.attachment('name,total\nengines,41')
.set_filename('figures.csv')
echo figures.filename()
echo figures.content_type()
echo figures.size()
figures.csv
text/csv
24
The media type follows from the filename unless it is set. The transfer encoding follows from the contents, and a file is always encoded rather than sent as it is, because bytes that happen to look like text are still bytes.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.Attachment(data)
Parameters
data(string|bytes) — The contents.
Attachment.from_file()
mail.Attachment.from_file(path: string) -> Attachment
Reads the contents from a file on disk, taking the name from the path.
note.attach(Attachment.from_file('reports/q3.pdf'))
Parameters
path(string)
Returns Attachment
Raises Error if the file cannot be read.
Attachment.set_filename()
mail.Attachment.set_filename(name: string) -> Attachment
Sets the name to offer the file under.
A name with characters a header cannot carry is encoded, and a long one is split across continuations, on the way out.
Parameters
name(string)
Returns Attachment — itself.
Attachment.set_content_type()
mail.Attachment.set_content_type(type: string) -> Attachment
Sets the media type, for when the filename does not settle it.
Parameters
type(string) — Asapplication/pdf.
Returns Attachment — itself.
Attachment.set_disposition()
mail.Attachment.set_disposition(kind: string) -> Attachment
Sets whether the part is offered as a file or shown where it sits.
embed() sets this to inline itself, so it is only worth setting for
a part that is neither quite.
Parameters
kind(string) —attachmentorinline.
Returns Attachment — itself.
Attachment.set_cid()
mail.Attachment.set_cid(id: string) -> Attachment
Sets the identifier HTML refers to this part by.
embed() invents one when there is none, so this is for a reference
that has to be predictable.
Parameters
id(string) — With or without the angle brackets.
Returns Attachment — itself.
Attachment.set_encoding()
mail.Attachment.set_encoding(name: string) -> Attachment
Sets the transfer encoding, overriding the one the contents would have chosen.
Parameters
name(string) — One ofmail.encoding.TRANSFER_ENCODINGS.
Returns Attachment — itself.
Attachment.set_description()
mail.Attachment.set_description(text: string) -> Attachment
Sets a human-readable description, which some mail clients show beside the file.
Parameters
text(string)
Returns Attachment — itself.
Attachment.filename()
mail.Attachment.filename() -> string|nil
The name this will be offered under, or nil.
Returns string|nil
Attachment.content_type()
mail.Attachment.content_type() -> string
The media type this will be sent as, worked out from the filename when it was not set.
Returns string
Attachment.disposition()
mail.Attachment.disposition() -> string
Whether the part is offered as a file or shown where it sits.
Returns string
Attachment.cid()
mail.Attachment.cid() -> string|nil
The identifier HTML refers to this part by, or nil.
Returns string|nil
Attachment.size()
mail.Attachment.size() -> number
How many bytes the contents are.
Returns number
Attachment.data()
mail.Attachment.data() -> bytes
The contents.
Returns bytes
Attachment.to_part()
mail.Attachment.to_part() -> Message
Turns this into the message part that goes on the wire.
attach() and embed() call it. It is here for a program building a
tree by hand.
Returns Message
Attachment.to_string()
mail.Attachment.to_string()
2026, Richard Ore and Zuri contributors
mail.pool
import mail
mail.pool, soimport mailis enough and the names are called asmail.pool.*.import mail.poolreaches the same definitions directly.
Running a mail server on more than one connection at a time.
Both servers here serve one connection to the end before taking the
next, which is the right shape for the protocols and the wrong shape for
more than one client. serve() puts a pool of isolates behind one
listening socket: the main isolate accepts, and each worker takes a
connection and sees it through.
import mail.pool
import .my_server
pool.serve(my_server.build, { host: '0.0.0.0', port: 143, workers: 8 })
build is a function in a module of its own, not a closure, because an
isolate resolves a module’s function by name on its own side and cannot
be handed one that closes over anything here.
# my_server.zu
import mail.imap { ImapServer, MaildirStore }
def build() {
var store = MaildirStore('/var/mail')
store.add_account('ann', 'secret')
return ImapServer({}, store)
}
An IMAP connection can be open for hours, so the pool is a ceiling on how many clients can be served at once, not on how fast they are served. Size it accordingly.
Functions
worker_main()
mail.pool.worker_main(connections, build)
What one worker isolate runs: build a server of its own, then serve whatever connections the acceptor hands it.
Called by serve(). It is public because an isolate has to be able to
find it by name.
Parameters
connections(Channel) — Accepted sockets arrive here.build(function(0)) — Returns this worker’s server.
start()
mail.pool.start(build, options: ?dict) -> Cluster
Starts a pool without running the accept loop, so the address is known before the first connection.
Parameters
build(function(0)) — A module’s function returning a server.options(?dict) —host,portandworkers.
Returns Cluster
serve()
mail.pool.serve(build, options: ?dict) -> Cluster
Starts a pool and runs it until something closes it.
Parameters
build(function(0))options(?dict) — Asstart().
Returns Cluster
Classes
Cluster
class mail.pool.Cluster
A running pool: the socket, the workers, and the loop feeding them.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.pool.Cluster(listener, connections, workers: list)
Parameters
listener(TcpStream)connections(Channel)workers(list) — The isolates serving them.
Cluster.address()
mail.pool.Cluster.address() -> SocketAddr
The address the pool is listening on.
Returns SocketAddr
Cluster.run()
mail.pool.Cluster.run() -> Cluster
Accepts connections and hands them out until close().
Returns Cluster — itself.
Cluster.accept_one()
mail.pool.Cluster.accept_one() -> Cluster
Accepts exactly one connection and hands it to a worker, for a program that wants to drive the loop itself.
Returns Cluster — itself.
Cluster.close()
mail.pool.Cluster.close(timeout: ?number) -> Cluster
Stops accepting, tells the workers there is nothing more coming, and waits for them to finish what they hold.
Parameters
timeout(?number) — Milliseconds to wait for each worker.
Returns Cluster — itself.
Cluster.to_string()
mail.pool.Cluster.to_string()
2026, Richard Ore and Zuri contributors
mail.pop3
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.pop3on its own works too and reaches the same definitions.
POP3: the client end of collecting mail.
import mail.pop3 { Pop3Client }
var mailbox = Pop3Client.connect('pop3s://mail.example.com', {
username: 'ann',
password: secret,
})
echo mailbox.stat().count
mailbox.quit()
Submodules
| Module | Reached as | Summary |
|---|---|---|
mail.pop3.client | mail.* | POP3: collecting mail and taking it away. |
2026, Richard Ore and Zuri contributors
mail.pop3.client
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.pop3.clienton its own works too and reaches the same definitions.
POP3: collecting mail and taking it away.
Where IMAP leaves mail on the server and lets a client work with it in place, POP3 hands it over and, usually, forgets it. That makes it the wrong protocol for reading mail on more than one device and the right one for a program whose job is to drain a mailbox.
import mail.pop3 { Pop3Client }
var mailbox = Pop3Client.connect('pop3s://mail.example.com', {
username: 'ann',
password: secret,
})
for entry in mailbox.list() {
var note = mailbox.retrieve(entry.number)
archive(note)
mailbox.delete(entry.number)
}
mailbox.quit()
Nothing is actually removed until quit(): delete() only marks, and
reset() undoes every mark. A connection that drops without quit()
leaves the mailbox as it was, which is the protocol protecting against a
client that fails halfway.
There is no POP3 server here. A POP3 server is an IMAP server with
almost everything taken away, and mail.imap is the one to run.
Constants
POP3_SCHEMES
mail.POP3_SCHEMES = {...}
Classes
Entry
class mail.Entry
One message in the mailbox, as list() reports it.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.Entry(number: number, size: number, uid: ?string)
Parameters
number(number) — Its position, which holds for this session only.size(number) — How many bytes it is.uid(?string) — The identifier that outlives the session, when the server offers one.
Entry.to_string()
mail.Entry.to_string()
Pop3Client
class mail.Pop3Client
A connection to a server that hands mail over.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.Pop3Client(connection, options: ?dict)
Builds a client around a connection that is already open.
Parameters
connection(LineStream)options(?dict) — Asconnect().
Pop3Client.connect()
mail.Pop3Client.connect(url: string, options: ?dict) -> Pop3Client
Opens a connection and authenticates.
| option | default | what it does |
|---|---|---|
username, password | none | authenticate when both are given |
tls | require | require, prefer or disable, when not already encrypted |
tls_config | a default one | the trust settings for TLS |
mechanisms | all of them | restricts which are acceptable |
timeout | 60000 | milliseconds to wait on the socket |
Parameters
url(string) —pop3s://host,pop3://host, or a bare host.options(?dict)
Returns Pop3Client
Raises Pop3Error if the server refuses.
Raises AuthenticationError if it refuses the credentials.
Pop3Client.begin()
mail.Pop3Client.begin() -> Pop3Client
Reads the greeting, negotiates TLS and authenticates.
Returns Pop3Client — itself.
Pop3Client.supports()
mail.Pop3Client.supports(name: string) -> bool
Whether the server offers a capability.
Parameters
name(string)
Returns bool
Pop3Client.is_secure()
mail.Pop3Client.is_secure() -> bool
Whether the connection is encrypted.
Returns bool
Pop3Client.refresh_capabilities()
mail.Pop3Client.refresh_capabilities() -> list
Asks the server what it can do.
A server old enough not to know CAPA reports nothing, which is not an
error: it simply predates the command.
Returns list — of string
Pop3Client.start_tls()
mail.Pop3Client.start_tls() -> Pop3Client
Negotiates TLS over a connection that started in the clear.
Returns Pop3Client — itself.
Raises Pop3Error if the server refuses.
Pop3Client.login()
mail.Pop3Client.login(mechanism: ?string) -> Pop3Client
Authenticates, choosing the strongest mechanism both ends know.
APOP is preferred over sending the password where the server’s
greeting offered it, and USER/PASS is the last resort and only over
TLS.
Parameters
mechanism(?string) — Forces one rather than choosing.
Returns Pop3Client — itself.
Raises AuthenticationError if the credentials are refused or
nothing usable is on offer.
Pop3Client.authenticate()
mail.Pop3Client.authenticate(mechanism) -> Pop3Client
Runs one authentication exchange with a mechanism already built.
Parameters
mechanism(Mechanism)
Returns Pop3Client — itself.
Raises AuthenticationError if the server refuses.
Pop3Client.stat()
mail.Pop3Client.stat() -> dict
How many messages are waiting and how large they are together.
Returns dict — of count and size.
Pop3Client.list()
mail.Pop3Client.list() -> list
Every message waiting, with its size and, where the server offers one, its lasting identifier.
The number is only good for this session: deleting a message renumbers the rest on the next connection. The identifier is what a program that runs twice should remember.
Returns list — of Entry
Pop3Client.uidl()
mail.Pop3Client.uidl()
The lasting identifier of every message, by its number in this session.
Returns — dict, empty when the server does not offer UIDL.
Pop3Client.retrieve()
mail.Pop3Client.retrieve(number: number) -> Message
Fetches one message whole.
Parameters
number(number)
Returns Message
Raises Pop3Error if there is no such message.
Pop3Client.retrieve_raw()
mail.Pop3Client.retrieve_raw(number: number) -> bytes
Fetches one message as it arrived, without parsing it.
Parameters
number(number)
Returns bytes
Raises Pop3Error if there is no such message.
Pop3Client.top()
mail.Pop3Client.top(number: number, lines: ?number) -> Message
Fetches a message’s headers and the first few lines of its body, which is enough to decide whether to fetch the rest.
Parameters
number(number)lines(?number) — Body lines to include. None when not given.
Returns Message
Raises Pop3Error if the server does not offer TOP.
Pop3Client.delete()
mail.Pop3Client.delete(number: number) -> Pop3Client
Marks a message for removal. Nothing goes until quit().
Parameters
number(number)
Returns Pop3Client — itself.
Raises Pop3Error if there is no such message.
Pop3Client.reset()
mail.Pop3Client.reset() -> Pop3Client
Unmarks everything marked for removal in this session.
Returns Pop3Client — itself.
Pop3Client.noop()
mail.Pop3Client.noop() -> Pop3Client
Asks the server for nothing, which keeps the connection alive.
Returns Pop3Client — itself.
Pop3Client.quit()
mail.Pop3Client.quit() -> Pop3Client
Says goodbye, which is when the server actually removes whatever was marked, and closes the connection.
Returns Pop3Client — itself.
Pop3Client.close()
mail.Pop3Client.close() -> Pop3Client
Closes the connection without saying goodbye, which leaves the mailbox exactly as it was.
Returns Pop3Client — itself.
Pop3Client.to_string()
mail.Pop3Client.to_string()
2026, Richard Ore and Zuri contributors
mail.sasl
import mail
mail.sasl, soimport mailis enough and the names are called asmail.sasl.*.import mail.saslreaches the same definitions directly.
The authentication mechanisms all three mail protocols share.
SMTP, IMAP and POP3 each have their own way of starting an authentication exchange and their own way of carrying the bytes, but what travels inside is the same in all three. This module holds the mechanisms themselves, so none of the protocols has to.
import mail.sasl
var mechanism = sasl.of('PLAIN', { username: 'ann', password: 'secret' })
echo mechanism.name()
echo mechanism.start().to_string().replace('/\0/', '|')
PLAIN
|ann|secret
What is here
| mechanism | what it sends |
|---|---|
PLAIN | the password, so it needs TLS under it |
LOGIN | the same, one prompt at a time, for servers that only do this |
CRAM-MD5 | a keyed digest of the server’s challenge |
SCRAM-SHA-1, SCRAM-SHA-256 | a proof the server checks without the password, and one back |
XOAUTH2, OAUTHBEARER | a bearer token, which is what the large providers want |
EXTERNAL | nothing: the client certificate already said who this is |
ANONYMOUS | nothing, on purpose |
The clients pick the strongest mechanism both ends offer, in the order
PREFERENCE gives.
Constants
PREFERENCE
mail.sasl.PREFERENCE: list = [...]
The mechanisms this implements, strongest first, which is the order a client picks from what a server offers.
SCRAM proves the password without sending it and proves the server
knew it too. CRAM-MD5 proves it without sending it but proves nothing
about the server, and its digest is old. The rest send something worth
protecting, which is why every client here refuses to use them without
TLS.
NEEDS_TLS
mail.sasl.NEEDS_TLS: list = [...]
The mechanisms that put the password itself on the wire, and so must never be used without TLS.
Functions
prepare()
mail.sasl.prepare(text: string) -> string
Prepares a username or password the way RFC 4013 says to, so that two spellings of the same characters authenticate the same.
The space mappings, the deletions and the prohibited characters are all applied. Unicode normalisation is not, so text that needs it must arrive already normalised; for anything typed on a keyboard in any Latin script this makes no difference at all.
Parameters
text(string)
Returns string
Raises AuthenticationError if the text holds a character that may
not appear in a credential.
response()
mail.sasl.response(username: string, password: string, challenge) -> string
The answer to a CRAM-MD5 challenge.
Used by the mechanism, and by a server checking one, which has to work out the same answer to compare against.
Parameters
username(string)password(string)challenge(string|bytes)
Returns string
of()
mail.sasl.of(name: string, credentials: dict) -> Mechanism
Builds a mechanism by name.
| credential | used by |
|---|---|
username, password | PLAIN, LOGIN, CRAM-MD5, SCRAM-* |
username, token | XOAUTH2, OAUTHBEARER |
authorize_as | PLAIN, EXTERNAL, SCRAM-* |
host, port | OAUTHBEARER |
trace | ANONYMOUS |
Parameters
name(string) — One ofPREFERENCE.credentials(dict)
Returns Mechanism
Raises AuthenticationError if the name is not one of them, or a
credential it needs is missing.
choose()
mail.sasl.choose(offered: list, credentials: dict, secure: bool, allowed: ?list) -> string|nil
Picks the mechanism to use.
Takes what the server offers and what the caller can supply, and returns
the strongest name both sides have, or nil when there is none.
Mechanisms that put the password on the wire are left out entirely when the connection is not encrypted, which is why a client that refuses to authenticate in the clear does not have to check for it separately.
import mail.sasl
echo sasl.choose(['PLAIN', 'LOGIN', 'CRAM-MD5'], { password: 'x' }, false)
echo sasl.choose(['PLAIN', 'LOGIN'], { password: 'x' }, true)
echo sasl.choose(['PLAIN', 'LOGIN'], { password: 'x' }, false)
CRAM-MD5
PLAIN
nil
Parameters
offered(list) — The names the server advertised.credentials(dict) — What the caller has to offer.secure(bool) — Whether the connection is encrypted.allowed(?list) — Restricts the choice to these names.
Returns string|nil
Classes
Mechanism
class mail.sasl.Mechanism
What every mechanism looks like from the outside.
A client calls start() once, sends whatever comes back if anything
does, and then calls step() with each challenge the server sends until
is_done().
- printable — has a
@to_string(), soechoandprint()show something useful
Mechanism.name()
mail.sasl.Mechanism.name() -> string
The name to offer the server.
Returns string
Mechanism.start()
mail.sasl.Mechanism.start() -> bytes|nil
The response to send before the server has said anything, or nil for a
mechanism that waits to be asked.
Returns bytes|nil
Mechanism.step()
mail.sasl.Mechanism.step(challenge) -> bytes|nil
The response to one challenge.
Parameters
challenge(bytes) — The server’s challenge, already decoded.
Returns bytes|nil
Mechanism.is_done()
mail.sasl.Mechanism.is_done() -> bool
Whether the exchange is over as far as this mechanism is concerned. A server may still have the last word.
Returns bool
Mechanism.to_string()
mail.sasl.Mechanism.to_string()
Plain
class mail.sasl.Plain < Mechanism
PLAIN: the username and the password, separated by zero bytes.
Everything worth having is on the wire, so this belongs under TLS and nowhere else.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.Plain(username: string, password: string, authorize_as: ?string)
Parameters
username(string)password(string)authorize_as(?string) — The account to act as, when it is not the one being authenticated.
Plain.name()
mail.sasl.Plain.name()
Plain.start()
mail.sasl.Plain.start()
Login
class mail.sasl.Login < Mechanism
LOGIN: the username and the password again, one prompt at a time.
Never standardised and entirely superseded by PLAIN, but some servers
offer nothing else.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.Login(username: string, password: string)
Parameters
username(string)password(string)
Login.name()
mail.sasl.Login.name()
Login.step()
mail.sasl.Login.step(challenge)
Login.is_done()
mail.sasl.Login.is_done()
CramMd5
class mail.sasl.CramMd5 < Mechanism
CRAM-MD5: a keyed digest of the server’s challenge, so the password
never travels.
The digest is old and the exchange proves nothing about the server, so prefer SCRAM where there is a choice.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.CramMd5(username: string, password: string)
Parameters
username(string)password(string)
CramMd5.name()
mail.sasl.CramMd5.name()
CramMd5.step()
mail.sasl.CramMd5.step(challenge)
CramMd5.is_done()
mail.sasl.CramMd5.is_done()
XOAuth2
class mail.sasl.XOAuth2 < Mechanism
XOAUTH2: a bearer token rather than a password, which is what Google
and Microsoft accept.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.XOAuth2(username: string, token: string)
Parameters
username(string) — The account the token is for.token(string) — The access token, without theBearerword.
XOAuth2.name()
mail.sasl.XOAuth2.name()
XOAuth2.start()
mail.sasl.XOAuth2.start()
XOAuth2.step()
mail.sasl.XOAuth2.step(challenge)
OAuthBearer
class mail.sasl.OAuthBearer < Mechanism
OAUTHBEARER: the standardised form of the same idea, from RFC 7628.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.OAuthBearer(username: string, token: string, host: ?string, port: ?number)
Parameters
username(string)token(string)host(?string) — The server being reached, which the token may be bound to.port(?number)
OAuthBearer.name()
mail.sasl.OAuthBearer.name()
OAuthBearer.start()
mail.sasl.OAuthBearer.start()
OAuthBearer.step()
mail.sasl.OAuthBearer.step(challenge)
External
class mail.sasl.External < Mechanism
EXTERNAL: the connection already established who this is, usually with
a client certificate.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.External(authorize_as: ?string)
Parameters
authorize_as(?string) — The account to act as. Empty means whichever one the certificate names.
External.name()
mail.sasl.External.name()
External.start()
mail.sasl.External.start()
Anonymous
class mail.sasl.Anonymous < Mechanism
ANONYMOUS: no identity at all, with an optional note saying who is
knocking.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.Anonymous(trace: ?string)
Parameters
trace(?string) — An address or a token, for the server’s log.
Anonymous.name()
mail.sasl.Anonymous.name()
Anonymous.start()
mail.sasl.Anonymous.start()
Scram
class mail.sasl.Scram < Mechanism
SCRAM-SHA-1 and SCRAM-SHA-256: the password is never sent, the
server never has to store it, and the server proves it knew it too.
This is the mechanism to use wherever a server offers it. Channel
binding, the -PLUS form, is not offered: it needs a value out of the
TLS session that nothing here exposes.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.sasl.Scram(username: string, password: string, digest: ?string, authorize_as: ?string, nonce: ?string)
Parameters
username(string)password(string)digest(?string) —sha256when not given, orsha1.authorize_as(?string)nonce(?string) — The client nonce. A fresh random one is generated when not given, which is what every real exchange wants; passing one is for reproducing a known exchange.
Raises AuthenticationError if digest is neither.
Scram.name()
mail.sasl.Scram.name()
Scram.start()
mail.sasl.Scram.start()
Scram.step()
mail.sasl.Scram.step(challenge)
Scram.is_done()
mail.sasl.Scram.is_done()
2026, Richard Ore and Zuri contributors
mail.smtp
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.smtpon its own works too and reaches the same definitions.
SMTP: the protocol that moves mail from where it was written to where it is kept.
Both ends are here. SmtpClient hands messages to a server;
SmtpServer is the server, and decides what to accept through handlers
of your own.
import mail.smtp { SmtpClient }
var server = SmtpClient.connect('smtp://mail.example.com', {
username: 'reports',
password: secret,
})
server.send(note, nil)
server.quit()
Submodules
| Module | Reached as | Summary |
|---|---|---|
mail.smtp.client | mail.* | The sending end of SMTP. |
mail.smtp.server | mail.* | The receiving end of SMTP. |
2026, Richard Ore and Zuri contributors
mail.smtp.client
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.smtp.clienton its own works too and reaches the same definitions.
The sending end of SMTP.
SmtpClient opens a connection, negotiates what the server can do,
authenticates, and hands messages over. Most programs want
mail.send(), which does all of that for one message and closes the
connection again; the client itself is for sending many, or for needing
the steps apart.
import mail.smtp { SmtpClient }
var server = SmtpClient.connect('smtp://mail.example.com', {
username: 'reports',
password: secret,
})
for note in queue {
server.send(note, nil)
}
server.quit()
Constants
SMTP_SCHEMES
mail.SMTP_SCHEMES = {...}
MAX_REPLY_LINE
mail.MAX_REPLY_LINE = 1002
Classes
Reply
class mail.Reply
One reply from the server: its code, and whatever it said with it.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.Reply(code: number, lines: list)
Parameters
code(number) — The three digit reply code.lines(list) — Each line of the reply, without its code.
Reply.is_positive()
mail.Reply.is_positive() -> bool
Whether the server accepted: a code in the 200s or 300s.
Returns bool
Reply.is_intermediate()
mail.Reply.is_intermediate() -> bool
Whether the server wants more from the client before it will say, which is the 300s.
Returns bool
Reply.is_transient()
mail.Reply.is_transient() -> bool
Whether this is a refusal that may not be one tomorrow.
Returns bool
Reply.is_permanent()
mail.Reply.is_permanent() -> bool
Whether this is a refusal that trying again will not change.
Returns bool
Reply.ok()
mail.Reply.ok(doing: ?string) -> Reply
Raises the error this reply stands for, or returns it when the server accepted.
Parameters
doing(?string) — What was being attempted, for the message.
Returns Reply — itself.
Raises SmtpTransientError|SmtpPermanentError
Reply.to_string()
mail.Reply.to_string()
SmtpClient
class mail.SmtpClient
A connection to a server that sends mail.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.SmtpClient(connection, options: ?dict)
Builds a client around a connection that is already open.
connect() is what opens one; this is for a connection made some other
way, and for tests that hand it a transcript instead of a socket.
Parameters
connection(LineStream)options(?dict) — Asconnect().
SmtpClient.connect()
mail.SmtpClient.connect(url: string, options: ?dict) -> SmtpClient
Opens a connection and gets as far as being ready to send.
The greeting is read, capabilities are asked for, TLS is negotiated if the connection did not start with it, and the client authenticates when given something to authenticate with.
| option | default | what it does |
|---|---|---|
username, password | none | authenticate when both are given |
token | none | authenticate with a bearer token instead |
tls | require | require, prefer or disable, when not already encrypted |
tls_config | a default one | the trust settings for TLS |
mechanisms | all of them | restricts which are acceptable |
client_name | the local hostname | the name to greet the server with |
timeout | 30000 | milliseconds to wait on the socket |
Parameters
url(string) —smtp://host,smtps://host, or a bare host. A username and password in the string are used when the options do not carry them.options(?dict)
Returns SmtpClient
Raises SmtpError if the server refuses the greeting.
Raises AuthenticationError if it refuses the credentials.
Raises ProtocolError if TLS is required and not offered.
SmtpClient.begin()
mail.SmtpClient.begin() -> SmtpClient
Reads the greeting, introduces the client, negotiates TLS and
authenticates. connect() calls this; a client built around an existing
connection has to.
Returns SmtpClient — itself.
SmtpClient.greeting()
mail.SmtpClient.greeting() -> Reply|nil
What the server said when the connection opened.
Returns Reply|nil
SmtpClient.capabilities()
mail.SmtpClient.capabilities() -> dict
What the server said it can do, by name. A capability with no value of
its own maps to true.
Returns dict
SmtpClient.supports()
mail.SmtpClient.supports(name: string) -> bool
Whether the server offers a capability.
Parameters
name(string) — Matched without regard to case.
Returns bool
SmtpClient.is_secure()
mail.SmtpClient.is_secure() -> bool
Whether the connection is encrypted.
Returns bool
SmtpClient.is_authenticated()
mail.SmtpClient.is_authenticated() -> bool
Whether the client has authenticated.
Returns bool
SmtpClient.max_size()
mail.SmtpClient.max_size() -> number|nil
The largest message the server said it will take, or nil when it did
not say.
Returns number|nil
SmtpClient.read_reply()
mail.SmtpClient.read_reply() -> Reply
Reads one reply, however many lines it runs to.
Returns Reply
Raises ProtocolError if what arrives is not a reply.
SmtpClient.command()
mail.SmtpClient.command(line: string) -> Reply
Sends a command and reads the reply it draws.
Parameters
line(string)
Returns Reply
SmtpClient.ehlo()
mail.SmtpClient.ehlo(name: ?string) -> Reply
Introduces the client and reads back what the server can do.
Falls back to the older HELO when the server does not understand
EHLO, in which case there are no capabilities to read.
Parameters
name(?string) — The name to give. The one from the options, or the local hostname, when not given.
Returns Reply
Raises SmtpError if the server refuses both greetings.
SmtpClient.start_tls()
mail.SmtpClient.start_tls() -> SmtpClient
Negotiates TLS over a connection that started in the clear, and introduces the client again, because everything the server said before the handshake has to be thrown away.
Returns SmtpClient — itself.
Raises SmtpError if the server refuses.
SmtpClient.login()
mail.SmtpClient.login(mechanism: ?string) -> SmtpClient
Authenticates, choosing the strongest mechanism both ends know.
Parameters
mechanism(?string) — Forces one rather than choosing.
Returns SmtpClient — itself.
Raises AuthenticationError if there is no mechanism in common, if
the credentials are refused, or if the only ones on offer would put the
password on an unencrypted connection.
SmtpClient.authenticate()
mail.SmtpClient.authenticate(mechanism) -> SmtpClient
Runs one authentication exchange with a mechanism already built.
Parameters
mechanism(Mechanism)
Returns SmtpClient — itself.
Raises AuthenticationError if the server refuses.
SmtpClient.mail_from()
mail.SmtpClient.mail_from(sender, options: ?dict) -> Reply
Starts a transaction by naming the sender.
Parameters
sender(string|Address) — The return path. An empty string is the null sender a bounce is sent from.options(?dict) —sizeto declare the message’s size, andbodyfor8BITMIMEorBINARYMIME.
Returns Reply
Raises SmtpError if the server refuses the sender.
SmtpClient.rcpt_to()
mail.SmtpClient.rcpt_to(recipient, options: ?dict) -> Reply
Adds one recipient to the transaction.
Parameters
recipient(string|Address)options(?dict) —notifyandoriginalfor DSN.
Returns Reply
Raises SmtpError if the server refuses the recipient.
SmtpClient.data()
mail.SmtpClient.data(body) -> Reply
Hands over the message itself and ends the transaction.
Line endings are normalised and a leading dot on any line is doubled, so a body containing a line with one dot on it cannot end the message early.
Parameters
body(string|bytes)
Returns Reply — the server’s verdict on the whole message.
Raises SmtpError if the server refuses either the command or the
message.
SmtpClient.bdat()
mail.SmtpClient.bdat(chunk, last: bool) -> Reply
Hands the message over in chunks, which is what CHUNKING is for.
Nothing is escaped, because a chunk carries its own length and has no
terminator to collide with. Only works where the server offers
CHUNKING.
Parameters
chunk(string|bytes)last(bool) — Whether this is the final chunk.
Returns Reply
Raises StateError if the server does not offer CHUNKING.
Raises SmtpError if the server refuses.
SmtpClient.send()
mail.SmtpClient.send(message, options: ?dict) -> Reply
Sends one message: the sender, the recipients, and the message itself, in one transaction.
The sender comes from the message’s From and the recipients from its
To, Cc and Bcc unless the options say otherwise. Bcc is removed
before the message goes, which is the whole point of it.
server.send(note, nil)
server.send(note, { from: 'bounces@example.com' })
Parameters
message(Message)options(?dict) —fromandtoto override the envelope, and anythingmail_from()andrcpt_to()accept.
Returns Reply — the server’s verdict.
Raises StateError if there is no sender or no recipient.
Raises SmtpError if the server refuses any part of it.
SmtpClient.send_raw()
mail.SmtpClient.send_raw(sender, recipients: list, body, options: ?dict) -> Reply
Sends a message that is already bytes, to recipients given here.
Parameters
sender(string|Address)recipients(list)body(string|bytes)options(?dict)
Returns Reply
Raises StateError if there are no recipients.
SmtpClient.rset()
mail.SmtpClient.rset() -> Reply
Abandons whatever transaction is in progress.
Returns Reply
SmtpClient.noop()
mail.SmtpClient.noop() -> Reply
Asks the server for nothing, which is how a connection is kept from going idle.
Returns Reply
SmtpClient.verify()
mail.SmtpClient.verify(address: string) -> Reply
Asks whether the server knows an address.
Most servers on the public internet refuse to answer this, because answering it hands an address list to whoever asks. A refusal is returned rather than raised, since it is an answer of a kind.
Parameters
address(string)
Returns Reply
SmtpClient.quit()
mail.SmtpClient.quit() -> Reply|nil
Says goodbye and closes the connection.
Returns Reply|nil — the server’s farewell, or nil when it had
already gone.
SmtpClient.close()
mail.SmtpClient.close() -> SmtpClient
Closes the connection without saying goodbye.
Returns SmtpClient — itself.
SmtpClient.to_string()
mail.SmtpClient.to_string()
2026, Richard Ore and Zuri contributors
mail.smtp.server
import mail
Everything here is re-exported by
import mailis enough and the names are called asmail.*. Importingmail.smtp.serveron its own works too and reaches the same definitions.
The receiving end of SMTP.
SmtpServer accepts connections, runs the ESMTP conversation, and asks
handlers of your own what to do at each stage: whether to take this
sender, this recipient, this message. It knows the protocol; everything
about policy is yours.
import mail.smtp { SmtpServer }
var server = SmtpServer({ port: 2525, hostname: 'mail.example.com' })
server.on_rcpt(@(session, recipient) {
if !recipient.domain.ends_with('example.com') {
return { code: 550, message: 'not a local address' }
}
})
server.on_data(@(session, raw) {
store.deliver(session.recipients, raw)
})
server.listen()
A handler that returns nothing accepts. One that returns a code and a message refuses, with those. One that raises is a failure on the server’s side, and the sender is told to try again later.
Constants
MECHANISMS
mail.MECHANISMS = [...]
DEFAULT_MAX_SIZE
mail.DEFAULT_MAX_SIZE = 35882577
DEFAULT_MAX_RECIPIENTS
mail.DEFAULT_MAX_RECIPIENTS = 100
MAX_ERRORS
mail.MAX_ERRORS = 10
MAX_DATA_LINE
mail.MAX_DATA_LINE = 4096
Classes
SmtpSession
class mail.SmtpSession
One connection, and everything known about it so far.
Handlers are given this and may put their own values on it through
state, which starts empty and lives as long as the connection.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.SmtpSession(peer: string, secure: bool)
sender is the Address the transaction is from, nil before one has
started, and an empty string for the null path a bounce comes from.
recipients is the Address values accepted so far.
Parameters
peer(string) — The address the connection came from.secure(bool) — Whether it is encrypted.
SmtpSession.is_authenticated()
mail.SmtpSession.is_authenticated() -> bool
Whether the client has authenticated.
Returns bool
SmtpSession.is_secure()
mail.SmtpSession.is_secure() -> bool
Whether the connection is encrypted.
Returns bool
SmtpSession.reset()
mail.SmtpSession.reset()
SmtpSession.to_string()
mail.SmtpSession.to_string()
SmtpServer
class mail.SmtpServer
A server that accepts mail.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.SmtpServer(options: ?dict)
| option | default | what it does |
|---|---|---|
host | 127.0.0.1 | the address to bind |
port | 25 | the port to bind |
hostname | the machine’s own | the name to greet clients with |
max_size | 35 MB | the largest message to take |
max_recipients | 100 | recipients one message may have |
timeout | 300000 | milliseconds a client may go quiet |
require_tls | false | refuse mail on an unencrypted connection |
require_auth | false | refuse mail from a client that has not authenticated |
banner | none | extra text on the greeting line |
Parameters
options(?dict)
SmtpServer.on_connect()
mail.SmtpServer.on_connect(handler) -> SmtpServer
Called when a connection opens, with the session. Refusing here closes the connection before the greeting.
Parameters
handler(function(1))
Returns SmtpServer — itself.
SmtpServer.on_mail()
mail.SmtpServer.on_mail(handler) -> SmtpServer
Called with the session and the sender Address when a transaction
starts.
Parameters
handler(function(2))
Returns SmtpServer — itself.
SmtpServer.on_rcpt()
mail.SmtpServer.on_rcpt(handler) -> SmtpServer
Called with the session and each recipient Address. This is where a
server decides whether it has anywhere to put the message.
Parameters
handler(function(2))
Returns SmtpServer — itself.
SmtpServer.on_data()
mail.SmtpServer.on_data(handler) -> SmtpServer
Called with the session and the message, as bytes, once the whole of it has arrived. This is where a message is delivered.
Parameters
handler(function(2))
Returns SmtpServer — itself.
SmtpServer.on_auth()
mail.SmtpServer.on_auth(handler) -> SmtpServer
Called with the session and { mechanism, username, password }.
Returning nothing accepts.
Without this handler the server advertises no authentication at all, and a client that tries anyway is told so.
Parameters
handler(function(2))
Returns SmtpServer — itself.
SmtpServer.on_password()
mail.SmtpServer.on_password(handler) -> SmtpServer
Called with a username, and returns that account’s password or nil.
CRAM-MD5 proves the password without sending it, which means the
server has to work out the same digest and therefore has to know the
password. A server that cannot supply one does not offer the mechanism.
Parameters
handler(function(1))
Returns SmtpServer — itself.
SmtpServer.on_close()
mail.SmtpServer.on_close(handler) -> SmtpServer
Called with the session when a connection closes, however it closed.
Parameters
handler(function(1))
Returns SmtpServer — itself.
SmtpServer.on_error()
mail.SmtpServer.on_error(handler) -> SmtpServer
Called with the error and the session when something inside the server fails. Without it, failures are silent and the client is told to try again later.
Parameters
handler(function(2))
Returns SmtpServer — itself.
SmtpServer.use_tls()
mail.SmtpServer.use_tls(cert_chain: string, private_key: string) -> SmtpServer
Gives the server a certificate, which is what lets it offer STARTTLS.
Parameters
cert_chain(string) — The certificate chain, PEM encoded.private_key(string) — The key, PEM encoded.
Returns SmtpServer — itself.
SmtpServer.set_tls_config()
mail.SmtpServer.set_tls_config(config) -> SmtpServer
Gives the server a TLS configuration built elsewhere.
Parameters
config(TlsConfig)
Returns SmtpServer — itself.
SmtpServer.is_secure()
mail.SmtpServer.is_secure() -> bool
Whether the server can offer STARTTLS.
Returns bool
SmtpServer.bind()
mail.SmtpServer.bind() -> SmtpServer
Binds the listening socket without accepting anything yet, so that the address is known before the first connection.
Returns SmtpServer — itself.
SmtpServer.address()
mail.SmtpServer.address() -> SocketAddr
The address the server is listening on, which is how a port of 0 is
turned into the one the system chose.
Returns SocketAddr
SmtpServer.accept()
mail.SmtpServer.accept() -> SmtpServer
Accepts one connection and serves it to the end.
Returns SmtpServer — itself.
SmtpServer.listen()
mail.SmtpServer.listen() -> SmtpServer
Accepts connections one after another until close().
One connection is served at a time. To serve several at once, run
serve() instead, which puts a pool of isolates behind the same socket.
Returns SmtpServer — itself.
SmtpServer.close()
mail.SmtpServer.close() -> SmtpServer
Stops the accept loop and closes the listening socket.
Returns SmtpServer — itself.
SmtpServer.is_listening()
mail.SmtpServer.is_listening() -> bool
Whether the accept loop is running.
Returns bool
SmtpServer.serve_connection()
mail.SmtpServer.serve_connection(client) -> SmtpServer
Serves one already-accepted connection to the end, then closes it.
This is the whole conversation: the greeting, every command, and the close. Call it directly to put a server behind a socket something else accepted.
Parameters
client(TcpStream)
Returns SmtpServer — itself.
SmtpServer.to_string()
mail.SmtpServer.to_string()
2026, Richard Ore and Zuri contributors
mail.stream
import mail
mail.stream, soimport mailis enough and the names are called asmail.stream.*.import mail.streamreaches the same definitions directly.
A line-oriented connection, which is what all three mail protocols are underneath.
SMTP, IMAP and POP3 all speak in lines ending with a carriage return and
a newline, mixed with runs of raw bytes whose length something else
announced. LineStream puts that over any of the transports net
provides, and lets one be swapped for another mid-conversation, which is
what STARTTLS is.
Constants
TLS_MODES
mail.stream.TLS_MODES: list = [...]
How TLS is treated on a connection that did not start encrypted:
require refuses to go on without it, prefer takes it when the server
offers it, and disable does not ask.
MAX_LINE
mail.stream.MAX_LINE = 65536
Functions
connect()
mail.stream.connect(host: string, port: number, options: dict) -> LineStream
Opens a connection to a host, in TLS or in the clear.
Parameters
host(string)port(number)options(dict) —timeoutin milliseconds,tlsto handshake immediately,tls_configandserver_name.
Returns LineStream
Raises Error if the connection or the handshake fails.
start_tls()
mail.stream.start_tls(stream, server_name: string, config) -> LineStream
Wraps a connection that started in the clear in TLS, which is what every
STARTTLS comes down to.
Parameters
stream(LineStream)server_name(string) — The name to check the certificate against.config(?TlsConfig)
Returns LineStream — the same one, now encrypted.
Raises Error if the handshake fails.
endpoint()
mail.stream.endpoint(url: string, schemes: dict) -> dict
Reads a connection string into the pieces needed to open it.
Accepts scheme://[user[:password]@]host[:port], and a bare
host[:port] for which the first scheme in schemes is assumed.
import mail.stream
var where = stream.endpoint('smtps://ann:secret@mail.example.com', {
smtp: { port: 587, tls: false },
smtps: { port: 465, tls: true },
})
echo '${where.host} ${where.port} ${where.tls} ${where.username}'
mail.example.com 465 true ann
Parameters
url(string)schemes(dict) — Each scheme’s default port and whether it means TLS from the first byte.
Returns dict — of scheme, host, port, tls, username and
password, the last two nil when the string carries none.
Raises ProtocolError if the scheme is not one of those given, or
there is no host.
Classes
LineStream
class mail.stream.LineStream
A connection that reads and writes lines.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
mail.stream.LineStream(transport, secure: ?bool)
Parameters
transport(any) — Anything withread(),write_all(),flush()andclose(). Every socket innetqualifies.secure(?bool) — Whether the transport is already encrypted. Worked out from the transport when not given.
LineStream.transport()
mail.stream.LineStream.transport() -> any
The transport underneath, for the calls this does not wrap.
Returns any
LineStream.is_secure()
mail.stream.LineStream.is_secure() -> bool
Whether what goes over this connection is encrypted.
Returns bool
LineStream.upgrade()
mail.stream.LineStream.upgrade(transport, secure: ?bool) -> LineStream
Replaces the transport with another, which is what a protocol does once it has negotiated TLS over a connection that started in the clear.
Parameters
transport(any)secure(?bool)
Returns LineStream — itself.
Raises ProtocolError if anything is still buffered, since bytes
read before the handshake cannot belong after it.
LineStream.read_line()
mail.stream.LineStream.read_line(limit: ?number) -> string
Reads one line.
The line break comes off, and a lone newline is accepted in place of the pair, because servers send one often enough that refusing would be refusing real mail.
Parameters
limit(?number) — The longest line to accept.MAX_LINEwhen not given.
Returns string
Raises ConnectionClosed if the connection ends mid-line.
Raises ProtocolError if the line runs past the limit.
LineStream.read_bytes()
mail.stream.LineStream.read_bytes(count: number) -> bytes
Reads exactly count bytes, whatever they contain.
Parameters
count(number)
Returns bytes
Raises ConnectionClosed if the connection ends first.
LineStream.has_buffered()
mail.stream.LineStream.has_buffered() -> bool
Whether anything is waiting in the buffer, without touching the transport.
Returns bool
LineStream.write()
mail.stream.LineStream.write(data) -> LineStream
Writes bytes as they are.
Parameters
data(string|bytes)
Returns LineStream — itself.
LineStream.write_line()
mail.stream.LineStream.write_line(text: string) -> LineStream
Writes one line and the break that ends it.
Parameters
text(string)
Returns LineStream — itself.
LineStream.flush()
mail.stream.LineStream.flush() -> LineStream
Pushes anything buffered by the transport out to the peer.
Returns LineStream — itself.
LineStream.close()
mail.stream.LineStream.close() -> LineStream
Closes the connection. Closing one that is already closed does nothing.
Returns LineStream — itself.
LineStream.is_closed()
mail.stream.LineStream.is_closed() -> bool
Whether close() has been called.
Returns bool
LineStream.to_string()
mail.stream.LineStream.to_string()
2026, Richard Ore and Zuri contributors
ffi
import ffi
Calling C and Rust libraries, and being called by them.
ffi loads a shared library, describes the functions in it, and calls
them with ordinary Zuri values. Every conversion across the boundary is
checked against the C type, so a value that does not fit is an error at
the call rather than a corrupted argument inside the library. The module
reads C and Rust declarations as their headers and crates write them,
links static libraries into loadable ones, and turns Zuri functions into
C function pointers.
import ffi
var c = ffi.open(ffi.LIBC).declare('
size_t strlen(const char *s);
int abs(int n);
')
echo c.strlen('hello')
echo c.abs(-7)
Values at the boundary
Numbers become any C integer or floating-point type, with integers
range-checked against their type. Integers too large for a number to
hold exactly, 64- and 128-bit ones, travel as bigints both ways. A
string passed to a char * becomes a NUL-terminated copy for the
duration of the call; bytes pass their own storage with nothing copied;
a list or dictionary passed to a pointer becomes a temporary array or
struct, written back afterwards unless the pointer is to const.
Structs and unions by value are dictionaries. nil is a null pointer,
and a null pointer coming back is nil.
Memory
alloc() and its siblings hand out memory that knows its own size, and
every access through a pointer into it is bounds-checked and refused
once the memory is freed. Memory C returns can be taken over with
Pointer.own(), which releases it with the right function when the
pointer is collected.
Callbacks
A Zuri function passed to a function-pointer parameter works as a
callback for that call. callback() makes one that lasts, for a library
that keeps the pointer. A callback can be called from any thread: from
another thread, the call waits until the isolate that made the callback
runs it.
The ffi API
Every public name in ffi, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
ffi.Callback | class | A Zuri function behind a C function pointer, made by ffi.callback(). |
ffi.CallbackError | class | A callback could not be created, or was used after its release. |
ffi.DeclarationError | class | C or Rust source handed to a declaration could not be read. |
ffi.Declarations | class | A set of declarations: types, functions, variables and constants, read from any mix of C and Rust source. |
ffi.EnumType | class | A C enum, or a Rust enum without fields. |
ffi.FfiError | class | Base class for every error this module raises. |
ffi.LIBC | constant | The C runtime library of this platform, as open() takes it: libc.so.6 on Linux, the system library on… |
ffi.LIBM | constant | The C maths library, which on macOS and Windows is the same library as LIBC. |
ffi.Library | class | A shared library loaded into the process, or the process itself. |
ffi.LinkError | class | A static library could not be linked into a loadable one. |
ffi.LoadError | class | A library could not be found or loaded. |
ffi.Pointer | class | An address in native memory. |
ffi.PointerError | class | Memory was touched in a way that would have been undefined in C. |
ffi.RecordType | class | A struct or a union, built member by member. |
ffi.StructType | class | A C struct, or a Rust #[repr(C)] struct or enum with fields. |
ffi.SymbolError | class | A library does not export a symbol that was asked for. |
ffi.Type | class | A C type. |
ffi.Typed | class | A value marked with the C type it should travel as. |
ffi.UnionType | class | A C union. |
ffi.alloc | function | Zeroed memory for count values of type, typed as type. |
ffi.alloc_bytes | function | size zeroed bytes, as an untyped pointer. |
ffi.alloc_string | function | A NUL-terminated copy of text in memory that lasts, typed as its code unit: char, char16_t, wchar_t… |
ffi.array | function | An array type. |
ffi.at | function | A pointer to a known address. |
ffi.bool | constant | bool: C’s _Bool and Rust’s bool, one byte; a Zuri bool. |
ffi.callback | function | A lasting C function pointer that calls function. |
ffi.char | constant | char: signed on x86-64 and on Apple’s Arm platforms, unsigned on Arm Linux, as the platform’s C compiler… |
ffi.char16_t | constant | char16_t, a UTF-16 code unit. |
ffi.char32_t | constant | char32_t, a UTF-32 code unit. |
ffi.complex_double | constant | double _Complex, as complex_float with doubles. |
ffi.complex_float | constant | float _Complex: a list of two numbers, [real, imaginary]. |
ffi.declarations | function | An empty set of declarations to add C and Rust source to. |
ffi.declare | function | A set of declarations read from C source; the same as declarations().declare(source). |
ffi.declare_rust | function | A set of declarations read from Rust source; the same as declarations().declare_rust(source). |
ffi.default_link_cache | function | The directory link() keeps linked libraries in by default: $XDG_CACHE_HOME/zuri/ffi (or… |
ffi.describe | function | What a foreign function is: { name, pointer, type, threaded, library }, where pointer is its address as a… |
ffi.double | constant | double, 64 bits. |
ffi.enum | function | A new enum type to add constants to. |
ffi.errno | function | errno as the most recent foreign call on this isolate left it. |
ffi.f32 | constant | Rust’s f32. |
ffi.f64 | constant | Rust’s f64. |
ffi.find | function | Where name would be loaded from, without loading it, or nil when it cannot be found. |
ffi.float | constant | float, 32 bits. |
ffi.function | function | A Zuri function that calls the C function at pointer with the signature of type. |
ffi.function_type | function | A function type, for callbacks and for calling function pointers. |
ffi.i128 | constant | Rust’s i128. |
ffi.i16 | constant | Rust’s i16. |
ffi.i32 | constant | Rust’s i32. |
ffi.i64 | constant | Rust’s i64. |
ffi.i8 | constant | Rust’s i8. |
ffi.int | constant | int, 32 bits. |
ffi.int128 | constant | __int128, a 128-bit integer; Rust’s i128. |
ffi.int16 | constant | int16_t. |
ffi.int32 | constant | int32_t. |
ffi.int64 | constant | int64_t. |
ffi.int8 | constant | int8_t. |
ffi.intptr_t | constant | intptr_t, 64 bits. |
ffi.is_foreign | function | Whether value is a foreign function this module made. |
ffi.isize | constant | Rust’s isize. |
ffi.last_error | function | On Windows, GetLastError() as the most recent foreign call on this isolate left it, cleared before the call… |
ffi.link | function | Links static libraries into a shared one with the platform’s linker, and loads it. |
ffi.long | constant | long: 64 bits, except on Windows, where it is 32. |
ffi.longdouble | constant | long double: 80-bit extended precision on x86-64 Linux and macOS, 128-bit quad precision on Arm Linux, and… |
ffi.longlong | constant | long long, 64 bits. |
ffi.malloc | function | size zeroed bytes from the C allocator, as an untyped pointer. |
ffi.open | function | Loads a shared library. |
ffi.platform | function | The facts about this platform that decide how C types are laid out: `{ os, arch, pointer_size, long_size,… |
ffi.pointer | function | A pointer type. |
ffi.ptr | constant | void *, the untyped pointer. |
ffi.ptrdiff_t | constant | ptrdiff_t, 64 bits. |
ffi.rust_char | constant | Rust’s char: four bytes holding a Unicode scalar value, which is a one-character string on the Zuri side. |
ffi.schar | constant | signed char. |
ffi.serve | function | Runs every callback called from another thread that is waiting for this isolate, returning how many ran. |
ffi.set_errno | function | Sets errno, for the rare API that reads it. |
ffi.short | constant | short, 16 bits. |
ffi.size_t | constant | size_t, 64 bits. |
ffi.slice | function | The #[repr(C)] struct of a pointer and a length that a Rust slice is passed across the C ABI as: `{ ptr,… |
ffi.ssize_t | constant | ssize_t, 64 bits. |
ffi.string | constant | const char * that converts to and from a string: a string passed in becomes a NUL-terminated UTF-8 copy for… |
ffi.struct | function | A new struct type to add members to. |
ffi.threaded | function | The same foreign function, run on a helper thread while the calling isolate keeps answering callbacks called… |
ffi.type | function | A type written as C writes it, using only built-in type names: 'unsigned long', 'const char *',… |
ffi.u128 | constant | Rust’s u128. |
ffi.u16 | constant | Rust’s u16. |
ffi.u32 | constant | Rust’s u32. |
ffi.u64 | constant | Rust’s u64. |
ffi.u8 | constant | Rust’s u8. |
ffi.uchar | constant | unsigned char. |
ffi.uint | constant | unsigned int, 32 bits. |
ffi.uint128 | constant | unsigned __int128; Rust’s u128. |
ffi.uint16 | constant | uint16_t. |
ffi.uint32 | constant | uint32_t. |
ffi.uint64 | constant | uint64_t. |
ffi.uint8 | constant | uint8_t. |
ffi.uintptr_t | constant | uintptr_t, 64 bits. |
ffi.ulong | constant | unsigned long: 64 bits, except on Windows, where it is 32. |
ffi.ulonglong | constant | unsigned long long, 64 bits. |
ffi.union | function | A new union type to add members to. |
ffi.ushort | constant | unsigned short, 16 bits. |
ffi.usize | constant | Rust’s usize. |
ffi.void | constant | void: the return type of a function that returns nothing. |
ffi.wchar_t | constant | wchar_t: 16 bits and unsigned on Windows, 32 bits elsewhere. |
ffi.wstring | constant | const wchar_t * that converts to and from a string, as string does in wchar_t’s encoding: UTF-16 on… |
Submodules
| Module | Reached as | Summary |
|---|---|---|
ffi.callback | ffi.callback.* | Zuri functions that C can call. |
ffi.declare | ffi.declare.* | Declarations read from C and Rust source. |
ffi.errors | ffi.* | Every error the ffi module raises, under one root. |
ffi.library | ffi.library.* | A loaded shared library, and binding what it exports. |
ffi.pointer | ffi.pointer.* | Addresses in native memory, and reading and writing through them. |
ffi.types | ffi.types.* | C types as values. |
Constants
LIBC
ffi.LIBC: string
The C runtime library of this platform, as open() takes it:
libc.so.6 on Linux, the system library on macOS, and the Universal C
Runtime, ucrtbase.dll, on Windows.
LIBM
ffi.LIBM: string
The C maths library, which on macOS and Windows is the same library as
LIBC.
void
ffi.void: Type
void: the return type of a function that returns nothing.
bool
ffi.bool: Type
bool: C’s _Bool and Rust’s bool, one byte; a Zuri bool.
char
ffi.char: Type
char: signed on x86-64 and on Apple’s Arm platforms, unsigned on Arm
Linux, as the platform’s C compiler has it.
schar
ffi.schar: Type
signed char.
uchar
ffi.uchar: Type
unsigned char.
short
ffi.short: Type
short, 16 bits.
ushort
ffi.ushort: Type
unsigned short, 16 bits.
int
ffi.int: Type
int, 32 bits.
uint
ffi.uint: Type
unsigned int, 32 bits.
long
ffi.long: Type
long: 64 bits, except on Windows, where it is 32.
ulong
ffi.ulong: Type
unsigned long: 64 bits, except on Windows, where it is 32.
longlong
ffi.longlong: Type
long long, 64 bits.
ulonglong
ffi.ulonglong: Type
unsigned long long, 64 bits.
int8
ffi.int8: Type
int8_t.
uint8
ffi.uint8: Type
uint8_t.
int16
ffi.int16: Type
int16_t.
uint16
ffi.uint16: Type
uint16_t.
int32
ffi.int32: Type
int32_t.
uint32
ffi.uint32: Type
uint32_t.
int64
ffi.int64: Type
int64_t. Values beyond 2^53 in magnitude read back as bigints.
uint64
ffi.uint64: Type
uint64_t. Values above 2^53 read back as bigints.
int128
ffi.int128: Type
__int128, a 128-bit integer; Rust’s i128.
uint128
ffi.uint128: Type
unsigned __int128; Rust’s u128.
size_t
ffi.size_t: Type
size_t, 64 bits.
ssize_t
ffi.ssize_t: Type
ssize_t, 64 bits.
ptrdiff_t
ffi.ptrdiff_t: Type
ptrdiff_t, 64 bits.
intptr_t
ffi.intptr_t: Type
intptr_t, 64 bits.
uintptr_t
ffi.uintptr_t: Type
uintptr_t, 64 bits.
wchar_t
ffi.wchar_t: Type
wchar_t: 16 bits and unsigned on Windows, 32 bits elsewhere.
char16_t
ffi.char16_t: Type
char16_t, a UTF-16 code unit.
char32_t
ffi.char32_t: Type
char32_t, a UTF-32 code unit.
float
ffi.float: Type
float, 32 bits.
double
ffi.double: Type
double, 64 bits.
longdouble
ffi.longdouble: Type
long double: 80-bit extended precision on x86-64 Linux and macOS,
128-bit quad precision on Arm Linux, and the same as double on Windows
and Apple’s Arm platforms. A Zuri number is a double, so a wider value
is rounded to the nearest double when read.
complex_float
ffi.complex_float: Type
float _Complex: a list of two numbers, [real, imaginary]. Passing
one by value is not available on Windows, whose C compiler has no
_Complex; in memory it works everywhere.
complex_double
ffi.complex_double: Type
double _Complex, as complex_float with doubles.
ptr
ffi.ptr: Type
void *, the untyped pointer.
string
ffi.string: Type
const char * that converts to and from a string: a string passed in
becomes a NUL-terminated UTF-8 copy for the call, and a pointer coming
back is read up to its terminator into a string, or is nil when null.
Use pointer(char) to receive the pointer itself.
wstring
ffi.wstring: Type
const wchar_t * that converts to and from a string, as string does
in wchar_t’s encoding: UTF-16 on Windows and UTF-32 elsewhere.
i8
ffi.i8: Type
Rust’s i8.
u8
ffi.u8: Type
Rust’s u8.
i16
ffi.i16: Type
Rust’s i16.
u16
ffi.u16: Type
Rust’s u16.
i32
ffi.i32: Type
Rust’s i32.
u32
ffi.u32: Type
Rust’s u32.
i64
ffi.i64: Type
Rust’s i64.
u64
ffi.u64: Type
Rust’s u64.
i128
ffi.i128: Type
Rust’s i128.
u128
ffi.u128: Type
Rust’s u128.
isize
ffi.isize: Type
Rust’s isize.
usize
ffi.usize: Type
Rust’s usize.
f32
ffi.f32: Type
Rust’s f32.
f64
ffi.f64: Type
Rust’s f64.
rust_char
ffi.rust_char: Type
Rust’s char: four bytes holding a Unicode scalar value, which is a
one-character string on the Zuri side.
Functions
open()
ffi.open(name, options) -> Library
Loads a shared library.
name is a path, a file name, or a bare name the platform’s naming
convention completes: sqlite3 is tried as libsqlite3.so and then as
the versioned file the loader’s cache lists on Linux, libsqlite3.dylib
and sqlite3.framework on macOS, and sqlite3.dll on Windows. The
directories in paths are searched first, then the platform’s own
search order.
With no name, the result is the running process itself, whose symbols include everything already loaded into it.
var libc = ffi.open(ffi.LIBC)
var local = ffi.open('mylib', { paths: ['./build'] })
Parameters
name(string|nil)options(dict|nil) —paths, a list of directories searched first;lazy, resolve symbols as they are first used rather than at load (defaultfalse);global, make the library’s symbols visible to libraries loaded after it (defaultfalse).lazyandglobalhave no effect on Windows.
Returns Library
Raises LoadError when the library cannot be found or loaded.
find()
ffi.find(name: string, paths) -> string|nil
Where name would be loaded from, without loading it, or nil when it
cannot be found. Resolves names the same way open() does.
Parameters
name(string)paths(list|nil) — Directories searched first.
Returns string|nil
declarations()
ffi.declarations() -> Declarations
An empty set of declarations to add C and Rust source to.
Returns Declarations
declare()
ffi.declare(source: string) -> Declarations
A set of declarations read from C source; the same as
declarations().declare(source).
Parameters
source(string)
Returns Declarations
Raises DeclarationError when the source cannot be read.
declare_rust()
ffi.declare_rust(source: string) -> Declarations
A set of declarations read from Rust source; the same as
declarations().declare_rust(source).
Parameters
source(string)
Returns Declarations
Raises DeclarationError when the source cannot be read.
struct()
ffi.struct(name) -> StructType
A new struct type to add members to.
Parameters
name(string|nil) — The name it prints as.
Returns StructType
union()
ffi.union(name) -> UnionType
A new union type to add members to.
Parameters
name(string|nil) — The name it prints as.
Returns UnionType
enum()
ffi.enum(name, type) -> EnumType
A new enum type to add constants to.
Parameters
name(string|nil) — The name it prints as.type(Type|nil) — The integer type values are stored as. Defaults toint, which is what C uses.
Returns EnumType
pointer()
ffi.pointer(type, options) -> Type
A pointer type.
Parameters
type(Type) — What it points at;voidfor an untyped pointer.options(dict|nil) —nonnull,trueto refusenilwhere one is passed, as a Rust reference does (defaultfalse);text, an encoding ('utf-8','utf-16','utf-32'or'wide') that makes the pointer convert to and from a string the wayffi.stringdoes.
Returns Type
array()
ffi.array(type, length) -> Type
An array type.
Parameters
type(Type) — The element type.length(number|nil) — Leave out for an array of unknown length.
Returns Type
function_type()
ffi.function_type(returns, params: list, options) -> Type
A function type, for callbacks and for calling function pointers.
var compare = ffi.function_type(ffi.int, [ffi.ptr, ffi.ptr])
Parameters
returns(Type)params(list) — The parameter types.options(dict|nil) —variadic(defaultfalse) andabi, as forLibrary.function().
Returns Type
slice()
ffi.slice(type, mutable) -> StructType
The #[repr(C)] struct of a pointer and a length that a Rust slice is
passed across the C ABI as: { ptr, len }, with len a usize
counting elements.
Parameters
type(Type) — The element type.mutable(bool|nil) — Whether the pointer is*mut; defaults to a*constone.
Returns StructType
type()
ffi.type(spelling: string) -> Type
A type written as C writes it, using only built-in type names:
'unsigned long', 'const char *', 'int[4]', 'void (*)(int)'.
Declarations.type() resolves names declared there too.
Parameters
spelling(string)
Returns Type
Raises DeclarationError when it does not name a type.
alloc()
ffi.alloc(type, count) -> Pointer
Zeroed memory for count values of type, typed as type.
Freed when the last pointer into it is collected, or by
Pointer.free().
var numbers = ffi.alloc(ffi.int, 4)
numbers.set(2, 99)
Parameters
type(Type)count(number|nil) — Defaults to 1.
Returns Pointer
Raises FfiError when the type has no size.
alloc_bytes()
ffi.alloc_bytes(size, align) -> Pointer
size zeroed bytes, as an untyped pointer.
Parameters
size(number)align(number|nil) — A power of two. Defaults to 16, enough for any C type.
Returns Pointer
malloc()
ffi.malloc(size) -> Pointer
size zeroed bytes from the C allocator, as an untyped pointer.
Unlike alloc(), this memory is never freed when the pointer is
collected, because the usual reason to want it is to hand it to C code
that frees it with free(). Free it with Pointer.free() otherwise.
Parameters
size(number)
Returns Pointer
alloc_string()
ffi.alloc_string(text: string, encoding) -> Pointer
A NUL-terminated copy of text in memory that lasts, typed as its code
unit: char, char16_t, wchar_t or char32_t.
Needed where a string must outlive a call: stored in a struct, or kept by the library.
Parameters
text(string)encoding(string|nil) —'utf-8'(the default),'utf-16','utf-32'or'wide'.
Returns Pointer
at()
ffi.at(address, type) -> Pointer
A pointer to a known address. Nothing about the address is checked.
Parameters
address(number|bigint)type(Type|nil) — What it points at.
Returns Pointer
function()
ffi.function(pointer, type, name) -> function
A Zuri function that calls the C function at pointer with the
signature of type.
Parameters
pointer(Pointer)type(Type) — A function type, or a pointer to one.name(string|nil) — The name the function reports.
Returns function
Raises PointerError when the pointer is null.
threaded()
ffi.threaded(function) -> function
The same foreign function, run on a helper thread while the calling isolate keeps answering callbacks called from other threads.
This is for a function that waits on threads of its own which call back into Zuri. Called the ordinary way, such a function would wait forever, because the isolate that has to run the callbacks is the one waiting for it.
Parameters
function(function) — A foreign function.
Returns function
Raises TypeError when function is not a foreign function.
is_foreign()
ffi.is_foreign(value) -> bool
Whether value is a foreign function this module made.
Parameters
value(any)
Returns bool
describe()
ffi.describe(function) -> dict
What a foreign function is: { name, pointer, type, threaded, library },
where pointer is its address as a Pointer, type its function
pointer type, and library the path of the library it came from, or
nil.
Parameters
function(function) — A foreign function.
Returns dict
callback()
ffi.callback(function, type, options) -> Callback
A lasting C function pointer that calls function.
var on_event = ffi.callback(@(code) {
echo 'event ${code}'
}, ffi.function_type(ffi.void, [ffi.int]))
lib.set_handler(on_event)
The callback lives until Callback.release(). When the Zuri function
raises, C gets error_value (or zero), and the error is raised again as
soon as control returns to Zuri.
Parameters
function(function)type(Type) — A function type, or a pointer to one.options(dict|nil) —error_value, returned to C whenfunctionraises. Defaults to zero, or nothing for avoidcallback.
Returns Callback
Raises TypeError when type is not a function type or is
variadic.
Raises CallbackError when 256 callbacks returning a 128-bit
integer under the Microsoft x64 convention are alive already; each one
released makes room for another.
serve()
ffi.serve(timeout) -> number
Runs every callback called from another thread that is waiting for this isolate, returning how many ran.
Such calls also run at the isolate’s own safepoints, during foreign calls and in threaded calls, so this is for a program that wants them answered at a point of its choosing, or wants to wait for one.
Parameters
timeout(number|nil) — Milliseconds to wait for a call when none is waiting. Returns at once when left out.
Returns number
errno()
ffi.errno() -> number
errno as the most recent foreign call on this isolate left it.
errno is cleared before every foreign call and read immediately after
it, so this is always what that one call set, or 0.
Returns number
set_errno()
ffi.set_errno(value)
Sets errno, for the rare API that reads it.
Parameters
value(number)
last_error()
ffi.last_error() -> number
On Windows, GetLastError() as the most recent foreign call on this
isolate left it, cleared before the call and read after it as errno
is. Always 0 on other platforms.
Returns number
link()
ffi.link(archives, options) -> Library
Links static libraries into a shared one with the platform’s linker, and loads it.
var lib = ffi.link('target/release/libgeometry.a')
Every object in the archives is linked in. The result is cached under a name derived from the archives’ contents and the options, so the linker runs once for a given input. An archive holding Rust code is recognised and linked against the system libraries Rust’s standard library needs.
The linker is the C compiler, cc or $CC, on Linux and macOS, and
MSVC’s link.exe on Windows, found through the Visual Studio
installation. On Windows a static library’s functions are not exported
by default, so every symbol the archives define with an unmangled name
is.
Parameters
archives(string|list) — One path or a list of them.options(dict|nil) —libraries, names to link against (['m']);search_paths, directories to find them in;exports, on Windows the exact symbols to export;linker, the program to run;flags, further arguments for it;cache, the directory results are kept in (defaultdefault_link_cache());rust, whether the archives hold Rust code (detected when left out).
Returns Library
Raises LinkError with the linker’s output when linking fails.
Raises LoadError when the linked library cannot be loaded.
default_link_cache()
ffi.default_link_cache() -> string
The directory link() keeps linked libraries in by default:
$XDG_CACHE_HOME/zuri/ffi (or ~/.cache/zuri/ffi) on Linux,
~/Library/Caches/zuri/ffi on macOS, and %LOCALAPPDATA%\zuri\ffi on
Windows.
Returns string
platform()
ffi.platform() -> dict
The facts about this platform that decide how C types are laid out: { os, arch, pointer_size, long_size, wchar_size, char_signed, long_double, complex, layout }.
long_double is 'x87', 'binary128' or 'double'; complex says
whether complex numbers can be passed by value; layout is 'msvc' on
Windows and 'itanium' elsewhere, the rules records follow.
Returns dict
2026, Richard Ore and Zuri contributors
ffi.callback
import ffi.callback
ffilifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledffi.callback.*needsimport ffi.callback.
Zuri functions that C can call.
Classes
Callback
class ffi.Callback
A Zuri function behind a C function pointer, made by ffi.callback().
A callback lives until release(). Nothing else frees it, because
nothing can know when the C library has finished with the pointer; a
callback that is never released is kept for the life of the program, and
so is the function behind it.
A Zuri function passed straight to a function-pointer parameter needs none of this: it becomes a callback for the length of that one call. Make one explicitly when C keeps the pointer past the call.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
ffi.Callback()
Callback.pointer()
ffi.Callback.pointer() -> Pointer
The C function pointer, typed with the callback’s signature.
Returns Pointer
Raises CallbackError when the callback has been released.
Callback.type()
ffi.Callback.type() -> Type
The callback’s type: a pointer to its function type.
Returns Type
Callback.release()
ffi.Callback.release()
Frees the callback’s code and lets go of its function.
C must not call the pointer again. Passing the callback to a foreign
function after this raises CallbackError; a call C still makes through
a pointer it kept is undefined, exactly as a call through any freed
function pointer is. Releasing twice does nothing.
Callback.is_released()
ffi.Callback.is_released() -> bool
Whether release() has been called.
Returns bool
Callback.to_string()
ffi.Callback.to_string()
2026, Richard Ore and Zuri contributors
ffi.declare
import ffi.declare
ffilifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledffi.declare.*needsimport ffi.declare.
Declarations read from C and Rust source.
Classes
Declarations
class ffi.Declarations
A set of declarations: types, functions, variables and constants, read from any mix of C and Rust source.
Each source added sees everything added before it, and anything in the
declarations it includes. Nothing is bound to a library until bind(),
so one set of declarations serves any number of libraries.
var api = ffi.declarations()
.declare('typedef struct { double x, y; } point;')
.declare('double length(point p);')
var geometry = api.bind(ffi.open('geometry'))
echo geometry.length({ x: 3, y: 4 })
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
ffi.Declarations()
Declarations.declare()
ffi.Declarations.declare(source: string)
Reads C declarations into the set, returning it for chaining.
Typedefs, structs, unions, enums, function prototypes, extern
variables and _Static_asserts are read. So are object-like #defines,
which become constants, and #if and its relatives, which are evaluated
for this platform. #pragma pack is honoured. Including a C standard
header is accepted, since everything it declares is already known;
including any other file is an error, as is expanding a function-like
macro.
A function returning const char * returns a string. A function with a
body, as an inline helper in a header has, is read and not bound, since
the library need not export it.
Parameters
source(string)
Returns — self
Raises DeclarationError with the line and column of the problem.
Declarations.declare_rust()
ffi.Declarations.declare_rust(source: string)
Reads Rust declarations into the set, returning it for chaining.
Functions in extern "C" blocks, and extern "C" fn definitions marked
#[no_mangle] or #[export_name], are read with their bodies skipped.
So are #[repr(C)], #[repr(transparent)], packed and align
structs and unions, enums with #[repr(C)] or an integer #[repr]
(with or without fields), type aliases, const items and extern
statics. #[cfg(...)] is evaluated for this platform. Anything else,
from use lines to impl blocks, is skipped, and a type may be used
before it is declared.
Parameters
source(string)
Returns — self
Raises DeclarationError with the line and column of the problem.
Declarations.include()
ffi.Declarations.include(other)
Makes the types and constants of other usable by sources added to this
set from now on, returning this set for chaining.
Parameters
other(Declarations)
Returns — self
Raises ValueError when that would make a set include itself.
Declarations.type()
ffi.Declarations.type(spelling: string) -> Type
A type written in C, resolved against these declarations: 'struct stat *',
'point[4]', 'int (*)(const void *, const void *)'.
Parameters
spelling(string)
Returns Type
Raises DeclarationError when it does not name a type.
Declarations.constant()
ffi.Declarations.constant(name: string) -> number|bigint|string|Pointer|nil
The value of a declared constant: an enum constant, a #define, or a
Rust const.
A #define casting a constant to a pointer type, as a header spells a
sentinel address, is a Pointer of that type at that address: ((void *) -1)
is the all-ones address, and a null one is nil. Passed where C takes
that pointer, it is exactly the value the C code would pass.
Parameters
name(string)
Returns number|bigint|string|Pointer|nil
Raises ValueError when no constant of that name was declared.
Declarations.constants()
ffi.Declarations.constants() -> dict
Every declared constant, by name, in the order declared.
Returns dict
Declarations.types()
ffi.Declarations.types() -> dict
Every named type: typedefs and Rust type names under their names, and
tagged C types under their tags, such as 'struct point'.
Returns dict
Declarations.functions()
ffi.Declarations.functions() -> dict
Every declared function’s type, by name.
Returns dict
Declarations.variables()
ffi.Declarations.variables() -> dict
Every declared variable’s type, by name.
Returns dict
Declarations.bind()
ffi.Declarations.bind(library, options) -> module
Binds the declarations to library, returning a namespace.
The namespace holds a function for each declared function, a Pointer
for each declared variable, each constant, and each type that has a name
of its own (a typedef or a Rust type); tagged C types are reached with
type(). Members are read the way a module’s are:
var sqlite = api.bind(ffi.open('sqlite3'))
echo sqlite.sqlite3_libversion()
Parameters
library(Library)options(dict|nil) —allow_missing: whentrue, a declared function or variable the library does not export is left out of the namespace instead of being an error. Defaults tofalse.
Returns module
Raises SymbolError naming every missing symbol, unless
allow_missing is set.
Declarations.to_string()
ffi.Declarations.to_string()
2026, Richard Ore and Zuri contributors
ffi.errors
import ffi
Everything here is re-exported by
ffi, soimport ffiis enough and the names are called asffi.*. Importingffi.errorson its own works too and reaches the same definitions.
Every error the ffi module raises, under one root.
FfiError is that root, and each subclass names the stage that failed:
finding a library, finding a symbol in it, reading a declaration,
touching memory, running a callback, or linking a static library. A
value that cannot be converted to the C type it is headed for is a
TypeError or RangeError from the prelude instead, the same errors
any other function raises for a bad argument, and the message names the
parameter and the type.
catch {
var sqlite = ffi.open('sqlite3')
} as error {
if instance_of(error, ffi.LoadError) {
echo 'SQLite is not installed: ${error.message}'
}
}
Classes
FfiError
class ffi.FfiError < Error
Base class for every error this module raises.
Catch this to catch anything ffi itself can do. Raised directly for a
type that cannot be used the way it was asked to be: a record with no
size passed by value, a signature libffi cannot call.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
ffi.FfiError(message)
Parameters
message(string)
FfiError.to_string()
ffi.FfiError.to_string()
LoadError
class ffi.LoadError < FfiError
A library could not be found or loaded.
The message carries the loader’s own explanation when it gave one: a missing dependency, a library built for another architecture, a file that is not a library at all. Also raised when a function is called after its library was closed.
- printable — has a
@to_string(), soechoandprint()show something useful
SymbolError
class ffi.SymbolError < FfiError
A library does not export a symbol that was asked for.
Raised by Library.function(), Library.variable() and by binding
declarations, which lists every name it could not find in one error.
- printable — has a
@to_string(), soechoandprint()show something useful
DeclarationError
class ffi.DeclarationError < FfiError
C or Rust source handed to a declaration could not be read.
line and column are both 1-based and point at the token the reader
stopped at, counted within the source string that was passed in.
catch {
ffi.declare('int add(int a, int b)')
} as error {
echo '${error.message} at ${error.line}:${error.column}'
}
- printable — has a
@to_string(), soechoandprint()show something useful
Fields
| Field | Type | Description |
|---|---|---|
line | number | The 1-based line the reader stopped at. |
column | number | The 1-based column within that line. |
Constructor
ffi.DeclarationError(message, line, column)
Parameters
message(string)line(number|nil)column(number|nil)
DeclarationError.to_string()
ffi.DeclarationError.to_string()
PointerError
class ffi.PointerError < FfiError
Memory was touched in a way that would have been undefined in C.
Reading or writing through a null pointer, past the end of a block the module allocated or was told the size of, through memory that has already been freed, freeing twice, or freeing memory this program does not own. The access is refused before it happens, so nothing has been corrupted when this is raised.
- printable — has a
@to_string(), soechoandprint()show something useful
CallbackError
class ffi.CallbackError < FfiError
A callback could not be created, or was used after its release.
An error raised by the Zuri function behind a callback is not wrapped in this. It is raised again, as it was, once the C code that called the callback returns.
- printable — has a
@to_string(), soechoandprint()show something useful
LinkError
class ffi.LinkError < FfiError
A static library could not be linked into a loadable one.
The message includes the linker’s own output.
- printable — has a
@to_string(), soechoandprint()show something useful
2026, Richard Ore and Zuri contributors
ffi.library
import ffi.library
ffilifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledffi.library.*needsimport ffi.library.
A loaded shared library, and binding what it exports.
Classes
Library
class ffi.Library
A shared library loaded into the process, or the process itself.
Libraries come from ffi.open() and ffi.link(). Functions are bound
from one either one at a time with function(), or in bulk from
declarations with declare(), declare_rust() and bind().
A library stays loaded while anything bound from it is still reachable,
even after close(): closing only stops further use.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
ffi.Library()
Library.path()
ffi.Library.path() -> string|nil
The path the library was loaded from, as the loader was given it, or
nil for the running process.
Returns string|nil
Library.close()
ffi.Library.close()
Stops the library being used. Every function bound from it raises
LoadError when called from now on. The code is unloaded once nothing
refers to the library any longer.
Library.is_closed()
ffi.Library.is_closed() -> bool
Whether close() has been called.
Returns bool
Library.function()
ffi.Library.function(name: string, returns, params: list, options) -> function
Binds one function by name and signature, returning it as a Zuri function.
var strlen = libc.function('strlen', ffi.size_t, [ffi.string])
echo strlen('hello')
For a variadic function, params lists only the fixed parameters. Each
extra argument travels as the type its value suggests: an integral
number that fits an int as int, a larger one as long long, any
other number as double, a bool as int, a string as const char *,
and a pointer, bytes or nil as void *. Give any other type with
Type.of().
On glibc, name@VERSION binds that version of a versioned symbol.
Parameters
name(string)returns(Type)params(list) — The parameter types.options(dict|nil) —variadic(defaultfalse) andabi, the calling convention:'default', or on x86-64'win64'or'sysv64'.
Returns function
Raises SymbolError when the library does not export name.
Raises FfiError when libffi cannot call the signature.
Library.variable()
ffi.Library.variable(name: string, type) -> Pointer
A pointer to a global variable the library exports, typed as the
variable, so get() reads it and set() writes it.
Parameters
name(string)type(Type)
Returns Pointer
Raises SymbolError when the library does not export name.
Library.symbol()
ffi.Library.symbol(name: string, type) -> Pointer|nil
The address of an exported symbol, or nil when there is none.
Parameters
name(string)type(Type|nil) — The type to give the pointer.
Returns Pointer|nil
Library.has()
ffi.Library.has(name: string) -> bool
Whether the library exports name.
Parameters
name(string)
Returns bool
Library.declare()
ffi.Library.declare(source: string, options) -> module
Reads C declarations and binds them to this library in one step,
returning the namespace Declarations.bind() does.
var c = libc.declare('
size_t strlen(const char *s);
int abs(int n);
')
echo c.strlen('hello')
Parameters
source(string)options(dict|nil) —allow_missingas forbind(), andinclude, a list ofDeclarationswhose types the source may use.
Returns module
Raises DeclarationError when the source cannot be read.
Raises SymbolError when a declared function or variable is
missing.
Library.declare_rust()
ffi.Library.declare_rust(source: string, options) -> module
Reads Rust declarations and binds them to this library in one step.
var geometry = ffi.open('geometry').declare_rust('
#[repr(C)]
pub struct Point { pub x: f64, pub y: f64 }
#[no_mangle]
pub extern "C" fn distance(a: Point, b: Point) -> f64 { 0.0 }
')
Parameters
source(string)options(dict|nil) — As fordeclare().
Returns module
Raises DeclarationError when the source cannot be read.
Raises SymbolError when a declared function or variable is
missing.
Library.bind()
ffi.Library.bind(declarations, options) -> module
Binds everything declarations holds to this library; the same as
declarations.bind(library, options).
Parameters
declarations(Declarations)options(dict|nil) — As forDeclarations.bind().
Returns module
Library.to_string()
ffi.Library.to_string()
2026, Richard Ore and Zuri contributors
ffi.pointer
import ffi.pointer
ffilifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledffi.pointer.*needsimport ffi.pointer.
Addresses in native memory, and reading and writing through them.
A Pointer is an address with, optionally, the type of what it points
at. The type is what get(), set() and the field methods read and
write, and the unit add() steps by; read() and write() take a type
of their own and work on any pointer.
Memory this module allocated knows its own extent, and every access through a pointer into it is checked against that extent and against the memory having been freed. A pointer C handed back carries no extent, so only a null pointer is caught; reading past what the C library says is there is as undefined as it is in C.
Classes
Pointer
class ffi.Pointer
An address in native memory.
Pointers come from ffi.alloc() and its siblings, from functions that
return one, and from reading a pointer out of memory. A null pointer
returned from C arrives as nil instead, so if ptr is how a program
checks for one.
Memory from ffi.alloc() is freed when the last pointer into it is
collected. Keep a pointer reachable for as long as C code holds the
address.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
ffi.Pointer()
Pointer.address()
ffi.Pointer.address() -> number|bigint
The address, as a number, or as a bigint where it is too large for a number to hold exactly.
Returns number|bigint
Pointer.is_null()
ffi.Pointer.is_null() -> bool
Whether the address is zero. A null pointer only exists as the result of
arithmetic or ffi.at(0); C returning one gives nil.
Returns bool
Pointer.type()
ffi.Pointer.type() -> Type|nil
The type of what the pointer points at, or nil for an untyped pointer
such as ffi.alloc_bytes() returns.
Returns Type|nil
Pointer.cast()
ffi.Pointer.cast(type) -> Pointer
The same address, pointing at type instead. Bounds and ownership carry
over. Pass nil for an untyped pointer.
Parameters
type(Type|nil)
Returns Pointer
Pointer.offset()
ffi.Pointer.offset(bytes) -> Pointer
The pointer bytes further on, negative for back. Keeps the type.
Parameters
bytes(number)
Returns Pointer
Pointer.add()
ffi.Pointer.add(count) -> Pointer
The pointer count elements further on, as C’s ptr + count is.
Parameters
count(number) — Negative steps back.
Returns Pointer
Raises TypeError when the pointer has no element type, or its
element type has no size.
Pointer.size()
ffi.Pointer.size() -> number|nil
How many bytes are known to be addressable from here to the end of the
block, or nil when the extent is unknown, as it is for any pointer C
handed back.
Returns number|nil
Pointer.read()
ffi.Pointer.read(type, offset) -> any
Reads a value of type at offset bytes from the pointer.
var header = ffi.alloc_bytes(16)
var magic = header.read(ffi.uint32)
var length = header.read(ffi.uint64, 8)
Parameters
type(Type)offset(number|nil) — Defaults to 0.
Returns any
Raises PointerError when the pointer is null, the memory is freed,
or the read runs outside a known block.
Pointer.write()
ffi.Pointer.write(type, value, offset)
Writes value as type at offset bytes from the pointer.
Nothing is written when the value does not convert, so a failed write never leaves half a value behind. Writing a record or array as a whole replaces every member; members the value leaves out keep what they held.
Parameters
type(Type)value(any)offset(number|nil) — Defaults to 0.
Raises PointerError for the reasons read() does.
Raises TypeError when value cannot be stored as type.
Raises RangeError when a number does not fit type.
Pointer.get()
ffi.Pointer.get(index) -> any
Reads element index of the pointer’s type, as C’s ptr[index].
Parameters
index(number|nil) — Defaults to 0.
Returns any
Raises TypeError when the pointer has no element type.
Raises PointerError for the reasons read() does.
Pointer.set()
ffi.Pointer.set(index, value)
Writes element index of the pointer’s type, as C’s ptr[index] = value.
Parameters
index(number)value(any)
Raises TypeError when the pointer has no element type or the value
does not convert.
Raises PointerError for the reasons read() does.
Pointer.get_field()
ffi.Pointer.get_field(name) -> any
Reads one member of the struct or union the pointer points at, as C’s
ptr->name. Bitfields read like any other member.
Parameters
name(string)
Returns any
Raises TypeError when the pointer does not point at a record.
Raises ValueError when the record has no such member.
Pointer.set_field()
ffi.Pointer.set_field(name, value)
Writes one member of the struct or union the pointer points at, as C’s
ptr->name = value, leaving the other members alone.
Parameters
name(string)value(any)
Raises TypeError when the pointer does not point at a record, or
the value does not convert.
Raises ValueError when the record has no such member.
Raises RangeError when a number does not fit, including a
bitfield’s width.
Pointer.field()
ffi.Pointer.field(name) -> Pointer
A pointer to one member, typed as the member, as C’s &ptr->name.
Reaching into nested records is a chain of these.
An array member gives a pointer to its first element instead, typed as the element, as the member itself decays to in C. That is how a flexible array member’s elements are reached.
Parameters
name(string)
Returns Pointer
Raises TypeError for a bitfield, which has no address.
Pointer.read_string()
ffi.Pointer.read_string(length, encoding, offset) -> string
Reads text.
With a length, exactly that many code units; without one, up to the
first zero code unit, which must lie within the block when the block’s
extent is known.
Parameters
length(number|nil) — In code units, not bytes.encoding(string|nil) —'utf-8'(the default),'utf-16','utf-32', or'wide'forwchar_t, which is UTF-16 on Windows and UTF-32 elsewhere.offset(number|nil) — In bytes. Defaults to 0.
Returns string
Raises ValueError when the text is not valid in its encoding.
Raises PointerError when there is no terminator inside a known
block.
Pointer.write_string()
ffi.Pointer.write_string(text, encoding, offset) -> number
Writes text and a terminating zero, returning the bytes written.
Parameters
text(string)encoding(string|nil) — As forread_string().offset(number|nil) — In bytes. Defaults to 0.
Returns number
Raises PointerError when the text and terminator do not fit a
known block.
Pointer.read_bytes()
ffi.Pointer.read_bytes(length, offset) -> bytes
Copies length bytes out.
Parameters
length(number)offset(number|nil) — Defaults to 0.
Returns bytes
Pointer.write_bytes()
ffi.Pointer.write_bytes(data, offset) -> number
Copies data in, returning how many bytes were written.
Parameters
data(bytes)offset(number|nil) — Defaults to 0.
Returns number
Pointer.to_list()
ffi.Pointer.to_list(count) -> list
Reads count elements of the pointer’s type into a list.
Parameters
count(number)
Returns list
Pointer.copy_from()
ffi.Pointer.copy_from(source, length)
Copies length bytes from source, a pointer or bytes, to here. The
two ranges may overlap.
Parameters
source(Pointer|bytes)length(number)
Pointer.fill()
ffi.Pointer.fill(byte, length)
Sets length bytes to byte.
Parameters
byte(number) — 0 to 255.length(number)
Pointer.compare()
ffi.Pointer.compare(other, length) -> number
Compares length bytes here with length bytes at other, as memcmp
does: -1, 0 or 1.
Parameters
other(Pointer)length(number)
Returns number
Pointer.own()
ffi.Pointer.own(destructor)
Takes ownership of memory C allocated, so it is released when the pointer is collected.
destructor is the C function that releases it, taking the address as
its one argument. Leave it out for memory from the C allocator, which
free releases. Pointers derived from this one afterwards share the
ownership.
A destructor run by the collector cannot call back into Zuri: any
callback it calls returns zero to C, or its error_value. free() runs
the destructor with callbacks working as usual.
var text = c.strdup('hello').own()
var db = sqlite.open_handle(path).own(sqlite.sqlite3_close)
Parameters
destructor(function|nil) — A foreign function of one pointer.
Returns — self
Raises PointerError when the pointer is null or already owned.
Raises TypeError when the destructor is not a foreign function of
one pointer.
Pointer.free()
ffi.Pointer.free()
Releases owned memory now rather than when it is collected: memory from
ffi.alloc() or ffi.malloc(), or taken over with own(). Every
pointer into it refuses access from then on.
Raises PointerError when the pointer does not own its memory, does
not point at the start of it, or it has already been freed.
Pointer.is_freed()
ffi.Pointer.is_freed() -> bool
Whether the memory this pointer owns has been freed.
Returns bool
Pointer.is_owned()
ffi.Pointer.is_owned() -> bool
Whether this pointer owns its memory: it came from ffi.alloc() or
ffi.malloc(), or own() was called on it or a pointer it came from.
Returns bool
Pointer.equals()
ffi.Pointer.equals(other) -> bool
Whether other is a pointer to the same address.
Parameters
other(any)
Returns bool
Pointer.to_string()
ffi.Pointer.to_string()
2026, Richard Ore and Zuri contributors
ffi.types
import ffi.types
ffilifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelledffi.types.*needsimport ffi.types.
C types as values.
Every type the module works with is a Type: the built-in ones the
module exports (ffi.int, ffi.double, ffi.size_t), the ones built
from them (ffi.pointer(ffi.char), ffi.array(ffi.int, 4)), records
built member by member, and every type a declaration names. A type knows
its size and alignment on this platform, and it is what every conversion
between a Zuri value and C memory is driven by.
Records are built with chained calls and are laid out the first time anything needs their size, after which they can no longer change:
var Point = ffi.struct('Point')
.add_field('x', ffi.double)
.add_field('y', ffi.double)
echo Point.size()
Classes
Type
class ffi.Type
A C type.
Types are never constructed directly. They come from the module’s constants and builders, and from declarations.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
ffi.Type()
Type.name()
ffi.Type.name() -> string
The type as C spells it: int, const char *, struct point, or the
name a typedef or a Rust declaration gave it.
Returns string
Type.kind()
ffi.Type.kind() -> string
What sort of type this is.
One of 'void', 'bool', 'int' (every integer and character type),
'float' (float, double and long double), 'complex', 'char'
(a Rust char), 'pointer', 'string' (a pointer that converts to and
from a string), 'function pointer', 'array', 'struct', 'union',
'enum' (C enums and Rust enums alike) and 'function'.
Returns string
Type.size()
ffi.Type.size() -> number
Size in bytes on this platform, as C’s sizeof gives it.
Asking for a record’s size lays the record out, after which no member can be added to it.
Returns number
Raises FfiError when the type has no size: void, a function, an
array without a length, or a record that was declared and never defined.
Type.align()
ffi.Type.align() -> number
Alignment in bytes on this platform, as C’s _Alignof gives it.
Returns number
Raises FfiError when the type has no alignment, for the same types
that have no size.
Type.is_const()
ffi.Type.is_const() -> bool
Whether the type is const-qualified.
A pointer to a const type is never written through, so a list or
dictionary passed for one is not updated after the call.
Returns bool
Type.target()
ffi.Type.target() -> Type|nil
The type this one is made from: what a pointer points at, what an array
holds, or the integer type an enum is stored as. nil for every other
type.
Returns Type|nil
Type.length()
ffi.Type.length() -> number|nil
An array type’s number of elements, or nil for any other type and for
an array declared without a length.
Returns number|nil
Type.signature()
ffi.Type.signature() -> dict|nil
The signature of a function type or a function pointer type, as { returns, params, variadic, abi },
or nil for any other type.
returns is a Type, params a list of them, variadic whether
arguments may follow the fixed ones, and abi the calling convention’s
name: 'default', 'win64' or 'sysv64'.
Returns dict|nil
Type.pointer()
ffi.Type.pointer() -> Type
A pointer to this type; the same as ffi.pointer(type).
Returns Type
Type.array()
ffi.Type.array(length) -> Type
An array of length values of this type; the same as ffi.array(type, length).
Parameters
length(number|nil) — Leave out for an array of unknown length, as a flexible array member is declared.
Returns Type
Type.as_const()
ffi.Type.as_const() -> Type
This type with const added.
Returns Type
Type.of()
ffi.Type.of(value) -> Typed
value marked as this type, for passing to a variadic function.
A variadic function’s extra arguments have no declared types, so each
one otherwise travels as the type its value suggests (see
Library.function()). This fixes it instead:
c.printf('%f %ld\n', ffi.double.of(1), ffi.long.of(7))
C’s default promotions still apply on top: a float travels as a
double, and an integer narrower than int as an int.
Parameters
value(any)
Returns Typed
Type.equals()
ffi.Type.equals(other) -> bool
Whether other is the same type.
Scalars, pointers and arrays compare by shape, so ffi.int32 equals
ffi.int, and a typedef equals what it names. Records and enums compare
by identity: two structs built separately are different types even with
the same members, as they are in C.
Parameters
other(any)
Returns bool
Type.to_string()
ffi.Type.to_string()
RecordType
class ffi.RecordType < Type
A struct or a union, built member by member.
Members go in declaration order, each at the offset the platform’s C compiler would give it: GCC and Clang’s rules on Linux and macOS, MSVC’s on Windows. The two differ only for bitfields. The record is laid out the first time anything needs its size, including passing a value of it, and from then on it cannot change.
- printable — has a
@to_string(), soechoandprint()show something useful
RecordType.add_field()
ffi.RecordType.add_field(name: string, type, align)
Adds a member, returning the record for chaining.
var Header = ffi.struct('Header')
.add_field('magic', ffi.uint32)
.add_field('payload', ffi.uint8.array(16))
.add_field('checksum', ffi.uint64, 16)
An array type without a length is a flexible array member and must come last. A struct or union added with an empty name is an anonymous member, whose own members are reached as this record’s.
Parameters
name(string)type(Type)align(number|nil) — A minimum alignment in bytes, as_Alignasgives a member. A power of two. Defaults to the type’s own.
Returns — self
Raises FfiError when the record has already been laid out, the
name is taken, or a member follows a flexible array member.
RecordType.add_bitfield()
ffi.RecordType.add_bitfield(name: string, type, bits: number)
Adds a bitfield, returning the record for chaining.
var Flags = ffi.struct('Flags')
.add_bitfield('ready', ffi.uint, 1)
.add_bitfield('mode', ffi.uint, 3)
.add_bitfield('', ffi.uint, 0)
.add_bitfield('count', ffi.uint, 12)
An empty name is an unnamed bitfield, which only takes up space; one of width zero ends the current storage unit, as in C.
Parameters
name(string)type(Type) — An integer,boolor enum type.bits(number) — The width, at most the type’s own.
Returns — self
Raises FfiError when the type is not an integer type or the width
does not fit it.
RecordType.set_packed()
ffi.RecordType.set_packed(bytes)
Packs the members to at most bytes alignment, as #pragma pack(n)
does, or tightly when bytes is left out, as __attribute__((packed))
does.
Parameters
bytes(number|nil) — 1, 2, 4, 8 or 16. Defaults to 1.
Returns — self
RecordType.set_align()
ffi.RecordType.set_align(bytes: number)
Raises the record’s own alignment to at least bytes, as
__attribute__((aligned(n))) on the record does. Its size grows to a
multiple of the new alignment.
Parameters
bytes(number) — A power of two.
Returns — self
RecordType.fields()
ffi.RecordType.fields() -> list
Every member in declaration order, with the members of anonymous members listed in their place.
Each is { name, type, offset, bits, bit_offset }. offset is in bytes
from the start of the record. For a bitfield, bits is its width and
bit_offset the position of its first bit from the start of the record;
both are nil for every other member.
Returns list
RecordType.offset_of()
ffi.RecordType.offset_of(name: string) -> number
A member’s offset in bytes, as C’s offsetof gives it. For a bitfield,
the byte its first bit is in.
Parameters
name(string)
Returns number
Raises ValueError when there is no such member.
RecordType.has_field()
ffi.RecordType.has_field(name: string) -> bool
Whether the record has a member called name.
Parameters
name(string)
Returns bool
RecordType.variants()
ffi.RecordType.variants() -> dict|nil
For a Rust enum with fields, each variant’s name and discriminant; nil
for an ordinary record.
Returns dict|nil
StructType
class ffi.StructType < RecordType
A C struct, or a Rust #[repr(C)] struct or enum with fields.
A struct value passes into C as a dictionary of its members and comes
back out as one. A member left out of the dictionary is zero. A Rust
enum with fields is a dictionary too, with a variant key naming the
variant, and a variant without fields may be passed as just its name.
- printable — has a
@to_string(), soechoandprint()show something useful
UnionType
class ffi.UnionType < RecordType
A C union.
A union value passes into C as a dictionary holding exactly one of its members, the one to store. Read back, it is a dictionary of every member, each read from the same bytes, since which one is meaningful is something only the program knows.
- printable — has a
@to_string(), soechoandprint()show something useful
EnumType
class ffi.EnumType < Type
A C enum, or a Rust enum without fields.
A value of an enum type is a number, and anywhere one is passed in, the name of a constant can be passed instead:
var Color = ffi.enum('Color').add_constant('RED', 0).add_constant('GREEN', 1)
set_color('GREEN')
- printable — has a
@to_string(), soechoandprint()show something useful
EnumType.add_constant()
ffi.EnumType.add_constant(name: string, value)
Adds a named constant, returning the enum for chaining.
Parameters
name(string)value(number) — An integer that fits the enum’s storage type.
Returns — self
Raises ValueError when the enum already has a constant of that
name.
Raises RangeError when the value does not fit.
EnumType.constants()
ffi.EnumType.constants() -> dict
Every constant, by name, in the order they were declared.
Returns dict
EnumType.value()
ffi.EnumType.value(name: string) -> number
The value of the constant called name.
Parameters
name(string)
Returns number
Raises ValueError when there is no such constant.
Typed
class ffi.Typed
A value marked with the C type it should travel as. Made by Type.of(),
and only meaningful as an argument to a variadic function.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
ffi.Typed(type, value)
Parameters
type(Type)value(any)
Typed.type()
ffi.Typed.type() -> Type
The type the value travels as.
Returns Type
Typed.value()
ffi.Typed.value() -> any
The value itself.
Returns any
Typed.to_string()
ffi.Typed.to_string()
2026, Richard Ore and Zuri contributors
types
import types
Provides type validation and conversion capabilities
This module is wrapper around the builtin functions where applicable and does and return the same thing as the builtin alternative.
The types API
Every public name in types, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
types.alpha | function | Returns true if the value is a character and alphabetic, otherwise returns false. |
types.bool | function | Returns true if the value is a boolean or false otherwise. |
types.bytes | function | Returns true if the value is a bytes or false otherwise. |
types.callable | function | Returns true if the value is a callable function or class and false otherwise. |
types.char | function | Returns true if the value is a single character or false otherwise. |
types.dict | function | Returns true if the value is a dictionary or false otherwise. |
types.digit | function | Returns true if the value is a character and digit, otherwise returns false. |
types.file | function | Returns true if the value is a file or false otherwise. |
types.function | function | Returns true if the value is a function or false otherwise. |
types.instance | function | Returns true if the value is an instance the given class, false otherwise. |
types.int | function | Returns true if the value is an integer or false otherwise. |
types.is_a_class | function | Returns true if the value is a class or false otherwise. |
types.iterable | function | Returns true if the value is an iterable or false otherwise. |
types.list | function | Returns true if the value is a list or false otherwise. |
types.number | function | Returns true if the value is a number or false otherwise. |
types.object | function | Returns true if the value is an object or false otherwise. |
types.of | function | Returns the name of the type of value |
types.string | function | Returns true if the value is a string or false otherwise. |
Functions
of()
types.of(value) -> string
Returns the name of the type of value
Parameters
value(any)
Returns string
Note: method implemented as part of core language features
digit()
types.digit(value: string) -> bool
Returns true if the value is a character and digit, otherwise returns false.
Parameters
value(string)
Returns bool
alpha()
types.alpha(value: string) -> bool
Returns true if the value is a character and alphabetic, otherwise returns false.
Parameters
value(string)
Returns bool
int()
types.int(value) -> bool
Returns true if the value is an integer or false otherwise.
Parameters
value(any)
Returns bool
bool()
types.bool(value) -> bool
Returns true if the value is a boolean or false otherwise.
Parameters
value(any)
Returns bool
number()
types.number(value) -> bool
Returns true if the value is a number or false otherwise.
Parameters
value(any)
Returns bool
Note: this method also returns true for integers.
char()
types.char(value) -> bool
Returns true if the value is a single character or false otherwise.
Parameters
value(any)
Returns bool
string()
types.string(value) -> bool
Returns true if the value is a string or false otherwise.
Parameters
value(any)
Returns bool
bytes()
types.bytes(value) -> bool
Returns true if the value is a bytes or false otherwise.
Parameters
value(any)
Returns bool
list()
types.list(value) -> bool
Returns true if the value is a list or false otherwise.
Parameters
value(any)
Returns bool
dict()
types.dict(value) -> bool
Returns true if the value is a dictionary or false otherwise.
Parameters
value(any)
Returns bool
object()
types.object(value) -> bool
Returns true if the value is an object or false otherwise.
Parameters
value(any)
Returns bool
function()
types.function(value) -> bool
Returns true if the value is a function or false otherwise.
Parameters
value(any)
Returns bool
is_a_class()
types.is_a_class(value) -> bool
Returns true if the value is a class or false otherwise.
Parameters
value(any)
Returns bool
file()
types.file(value) -> bool
Returns true if the value is a file or false otherwise.
Parameters
value(any)
Returns bool
iterable()
types.iterable(value) -> bool
Returns true if the value is an iterable or false otherwise.
Parameters
value(any)
Returns bool
callable()
types.callable(value) -> bool
Returns true if the value is a callable function or class and false otherwise.
Parameters
value(any)
Returns bool
instance()
types.instance(value, type: type) -> bool
Returns true if the value is an instance the given class, false otherwise.
Parameters
value(any)type(type)
Returns bool
2021, Richard Ore and Zuri contributors
enum
import enum
This module provides Zuri’s implementation of enumerations as described in the language documentation: a set of unique values bound to symbolic names via an alias.
Creating an enumeration
Initialize from a list of symbolic names; each is automatically assigned an ordinal value starting from zero:
import enum
var Gender = enum(['Male', 'Female'])
echo Gender.Male # 0
echo Gender.Female # 1
Initialize from a dictionary to bind specific values (numbers or strings) instead of automatic ordinals:
var Color = enum({
Red: 'r',
Green: 'g',
Blue: 'b',
})
echo enum.to_string(Color) # <Enum Red=r Green=g Blue=b>
By default, duplicate values in a dictionary-initialized enumeration
raise an error. Pass true as the second argument to allow them:
var Speed = enum({
Slow: 1,
Sluggish: 1,
Fast: 2,
}, true)
The enum API
Every public name in enum, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
enum.ensure | function | Returns value unchanged if it is a valid value for the enumeration, or raises an Error if it is not. |
enum.enum | function | Creates a new enumeration from either a list of symbolic names or a dictionary of symbolic-name to value… |
enum.has | function | Returns true if the enumeration contains the given symbolic key, or false otherwise. |
enum.keys | function | Returns the symbolic keys of an enumeration, in the order they were declared. |
enum.to_dict | function | Returns the enumeration as a key/value dictionary. |
enum.to_string | function | Returns a string representation of the enumeration. |
enum.to_value_dict | function | Returns the enumeration as a value/key dictionary. |
enum.values | function | Returns the possible values of an enumeration, in the order their corresponding keys were declared. |
Functions
enum()
enum.enum(source, allow_duplicates) -> dict
Creates a new enumeration from either a list of symbolic names or a dictionary of symbolic-name to value pairs.
When source is a list, every item must be a string; each is bound, in
order, to an automatically assigned ordinal value starting at zero.
When source is a dictionary, its keys must be strings and its values
must be numbers or strings. Duplicate values are rejected unless
allow_duplicates is true.
Enumeration keys are always unique; since the source is itself a dictionary or list, a duplicated symbolic name is never possible to express in the first place.
Parameters
source(list[string]|dict)allow_duplicates(?bool) — Default value isfalse.
Returns dict
Raises TypeError: If source is not a list or dictionary, or its
entries are of the wrong type.
Raises Error: If a duplicate value is found and allow_duplicates
is not true.
keys()
enum.keys(e) -> list[string]
Returns the symbolic keys of an enumeration, in the order they were declared.
Parameters
e(dict)
Returns list[string]
Note: Equivalent to calling
.keys()directly on the enum object, since an enumeration is itself a dictionary.
values()
enum.values(e) -> list[number|string]
Returns the possible values of an enumeration, in the order their corresponding keys were declared.
Parameters
e(dict)
Returns list[number|string]
Note: Equivalent to calling
.values()directly on the enum object.
to_dict()
enum.to_dict(e) -> dict
Returns the enumeration as a key/value dictionary.
Parameters
e(dict)
Returns dict
to_value_dict()
enum.to_value_dict(e) -> dict
Returns the enumeration as a value/key dictionary.
Since dictionaries cannot contain duplicate keys, if multiple enumeration keys share the same value, only the LAST declared key for that value is represented in the result; matching the documented behavior.
Parameters
e(dict)
Returns dict
has()
enum.has(e, key) -> bool
Returns true if the enumeration contains the given symbolic key, or
false otherwise.
Parameters
e(dict)key(string)
Returns bool
Note: Equivalent to calling
.contains(key)directly on the enum object.
ensure()
enum.ensure(e, value) -> any
Returns value unchanged if it is a valid value for the enumeration, or
raises an Error if it is not.
Parameters
e(dict)value(any)
Returns any
Raises Error: If value is not one of the enumeration’s values.
to_string()
enum.to_string(e) -> string
Returns a string representation of the enumeration.
Parameters
e(dict)
Returns string
2026, Richard Ore and The Zuri Contributors
convert
import convert
Data conversion between hexadecimal, arbitrary numeric bases, bytes, and decimal numbers.
The convert API
Every public name in convert, wherever it is declared. Each links to
the page that documents it.
| Name | Kind | Summary |
|---|---|---|
convert.binary_to_decimal | function | Converts a binary (base 2) string to a decimal number. |
convert.bytes_to_decimal | function | Converts bytes (binary data) to a decimal number, treating the bytes as an unsigned integer. |
convert.bytes_to_hex | function | Converts binary data (bytes) of any length to its hexadecimal string representation. |
convert.decimal_to_binary | function | Converts a decimal number to a binary (base 2) string. |
convert.decimal_to_bytes | function | Converts a decimal number to bytes, the reverse of bytes_to_decimal(). |
convert.decimal_to_hex | function | Converts the given decimal number to a hexadecimal string. |
convert.decimal_to_octal | function | Converts a decimal number to an octal (base 8) string. |
convert.from_base | function | Converts a string in the given base back to a decimal number. |
convert.hex_to_bytes | function | Converts a hexadecimal string of any (even) length to bytes. |
convert.hex_to_decimal | function | Converts a hexadecimal string to a decimal (base 10) number. |
convert.octal_to_decimal | function | Converts an octal (base 8) string to a decimal number. |
convert.to_base | function | Converts a decimal number to a string in the given base, using 0-9 then a-z for digits beyond 9 (so… |
convert.unicode_to_hex | function | Converts a single unicode character to its hexadecimal code point. |
Functions
hex_to_bytes()
convert.hex_to_bytes(str) -> bytes
Converts a hexadecimal string of any (even) length to bytes.
Parameters
str(string)
Returns bytes
Raises TypeError if str isn’t a string.
Raises ValueError if str has an odd number of characters: a hex
string always encodes whole bytes, so an odd length is ambiguous rather
than something this function guesses at.
bytes_to_hex()
convert.bytes_to_hex(data) -> string
Converts binary data (bytes) of any length to its hexadecimal string representation.
Parameters
data(bytes)
Returns string
Raises TypeError if data isn’t bytes.
decimal_to_hex()
convert.decimal_to_hex(n, digits) -> string
Converts the given decimal number to a hexadecimal string. If digits
is given and the result is shorter, it’s padded with leading zeros; if
the result is longer, it’s truncated to the least-significant digits
characters.
Parameters
n(number)digits(?number)
Returns string
Raises TypeError if n isn’t a number.
Raises ValueError if digits is given but isn’t a number.
hex_to_decimal()
convert.hex_to_decimal(str) -> number
Converts a hexadecimal string to a decimal (base 10) number.
Parameters
str(string)
Returns number
Raises TypeError if str isn’t a string.
Raises ValueError if str contains a character that isn’t a valid
hex digit.
Note:
strmay either be a plain hex string or carry a leading0xprefix.
unicode_to_hex()
convert.unicode_to_hex(chr) -> string
Converts a single unicode character to its hexadecimal code point.
Parameters
char— chr
Returns string
Raises ValueError if chr isn’t exactly one character.
decimal_to_binary()
convert.decimal_to_binary(n, digits) -> string
Converts a decimal number to a binary (base 2) string.
Parameters
n(number)digits(?number) — Zero-pads (or truncates) to this many characters, same asdecimal_to_hex()’sdigitsparameter.
Returns string
binary_to_decimal()
convert.binary_to_decimal(str) -> number
Converts a binary (base 2) string to a decimal number.
Parameters
str(string)
Returns number
Raises ValueError if str contains a character other than 0 or
1.
decimal_to_octal()
convert.decimal_to_octal(n, digits) -> string
Converts a decimal number to an octal (base 8) string.
Parameters
n(number)digits(?number) — Same asdecimal_to_hex()’sdigitsparameter.
Returns string
octal_to_decimal()
convert.octal_to_decimal(str) -> number
Converts an octal (base 8) string to a decimal number.
Parameters
str(string)
Returns number
Raises ValueError if str contains a character outside 0-7.
to_base()
convert.to_base(n, base, digits) -> string
Converts a decimal number to a string in the given base, using 0-9
then a-z for digits beyond 9 (so base 16 produces lowercase hex,
matching decimal_to_hex()). If digits is given and the result is
shorter, it’s padded with leading zeros; if longer, it’s truncated to
the least-significant digits characters.
Parameters
n(number)base(number) — Between2and36.digits(?number)
Returns string
Raises TypeError if n isn’t a number.
Raises ValueError if base is outside 2-36, or if digits is
given but isn’t a number.
from_base()
convert.from_base(str, base) -> number
Converts a string in the given base back to a decimal number. Accepts
an optional leading - for a negative value, and digit letters in
either case.
Parameters
str(string)base(number) — Between2and36.
Returns number
Raises TypeError if str isn’t a string.
Raises ValueError if base is outside 2-36, or if str
contains a character that isn’t a valid digit in that base.
bytes_to_decimal()
convert.bytes_to_decimal(data, little_endian) -> number
Converts bytes (binary data) to a decimal number, treating the bytes as an unsigned integer.
Parameters
data(bytes)little_endian(?bool) — Defaultfalse: the most significant byte comes first. Settrueto instead treat the least significant byte as first, as many binary file formats and network protocols do.
Returns number
Raises TypeError if data isn’t bytes.
decimal_to_bytes()
convert.decimal_to_bytes(n, length, little_endian) -> bytes
Converts a decimal number to bytes, the reverse of bytes_to_decimal().
Parameters
n(number) — Must be non-negative: there’s no signedness convention (two’s complement vs. sign-magnitude) implied here, so a negative value is rejected rather than guessed at.length(number) — The exact number of bytes to produce. Ifndoesn’t fit inlengthbytes, the most significant (excess) bits are silently dropped, the same as fitting a number into a fixed-width integer type would.little_endian(?bool) — Defaultfalse: the most significant byte is written first. Settruefor the least significant byte first.
Returns bytes
Raises TypeError if n isn’t a number.
Raises ValueError if n is negative, or length isn’t a
non-negative number.
2021, Richard Ore and Zuri contributors
zuri
import zuri
Exposes Zuri’s own compiler pipeline as a library: lexing, parsing, and
compiling a Zuri source file, plus a reflect submodule for inspecting
live functions, classes, and modules at runtime.
tokenize(source)returns every lexical token insource, in order; seezuri.token.parse(source)returnssource’s AST as a list of nodes, with every comment and doc block preserved in place; seezuri.ast.compile(source)returnssource’s compiled VM instructions; seezuri.compile.parse_partial(source)reads as much ofsourceas it can and returns the tree along with every syntax error, andcheck(source)returns every problem compilingsourcewould report, both aszuri.Diagnostics and neither raising on bad source; seezuri.astandzuri.compile.zuri.docreads documentation comments: the prose and@tags of a doc block, and which declaration each block documents; seezuri.doc.zuri.reflectinspects live functions, classes and modules, reads or updates an instance’s own properties, and runs the garbage collector on demand; seezuri.reflect.
The ordinary starting point in practice is a real file on disk, not a
source string already sitting in a variable, so each of the above also
has a _file counterpart that reads path first:
tokenize_file(path), parse_file(path), compile_file(path),
parse_partial_file(path) and check_file(path). There’s one more,
dump_file(path), with no bare-source equivalent of its own; it reads
and parses path, then returns the dump() of every top-level node,
which is usually the single fastest way to answer “what’s actually in
this file”.
The mental model
tokenize()/parse()/compile() all return the same SHAPE of thing: a
list of small, uniformly-tagged records. A token has a kind; an AST
node has a kind; an instruction has an op. Whichever one it is,
everything specific to that particular kind lives in one place:
token.value, node.fields, instr.fields. There’s a Token class, a
Node class, and an Instr class, not one class per token kind /
grammar rule / opcode. Zuri’s grammar alone has around fifty
expression/statement/declaration shapes, and the instruction set has
roughly sixty opcodes; a dedicated class for each would be well over a
hundred near-identical classes to keep in sync with the compiler forever
after, for what would ultimately still be a name + a handful of fields
each. This is the same tradeoff tools like Python’s ast module or
Babel’s ESTree make, for the same reason.
The real cost of that choice: nothing points you at what fields a given
kind/op carries the way a typed class would. You can’t autocomplete
your way to a Binary node’s left/op/right. So this module leans
hard on a different way of finding out: run something and look, rather
than read a spec first.
import zuri
var nodes = zuri.parse('def add(a, b) { return a + b }')
echo nodes[0].dump()
Function@1:1
name: 'add'
name_span: 1:5-1:8
parameters:
Argument@1:9
name: 'a'
name_span: 1:9-1:10
type_hint:
TypeHint@1:10
types:
Any@1:10
class_name: nil
nullable: true
...
body:
Block@1:15
statements:
Return@1:17
value:
Binary@1:24
left:
Identifier@1:24
name: 'a'
op: 'Plus'
right:
Identifier@1:28
name: 'b'
is_variadic: false
That single dump() call answers “what does a Function node look
like” far more completely than a paragraph of prose would, and it
answers it for whatever kind you’re actually holding, not just the ones
someone thought to write up.
zuri.ast.Node.dump and
zuri.compile.Instr.dump both work this
way; zuri.token.Token doesn’t need the same
treatment, since a token’s own fields (kind, line, column,
start, end, text, value) are always the same seven, spelled out
in zuri.token’s own docs.
Once you know the kind/op you’re after, walk_nodes()/
walk_instrs() (and their shorthand, find_nodes()/ find_instrs())
do the actual work. Almost nothing real gets done by manually indexing
into fields node by node; the normal shape of a task is “for every
node/instruction of kind X, do Y”, regardless of how deep in the tree it
sits (inside a nested function, inside an if, wherever), which is
exactly what these two functions are for, so reach for them first.
import zuri
var nodes = zuri.parse(source)
# every function name declared anywhere in the file, including
# nested/inner functions
var names = []
zuri.walk_nodes(nodes, {
Function: @(node) { names.append(node.fields.name) },
})
# shorthand for the common "just collect them" case
var comments = zuri.find_nodes(nodes, 'Comment')
So, end to end, the actual workflow is:
- Call
parse_file()/compile_file()on something real (orparse()/compile(), if you already have the source as a string).echo zuri.dump_file(path)first, if you just want to look before writing any real code against it. 2.dump()the specific node/instruction you care about, to see its actual shape. 3.walk_nodes()/walk_instrs()(orfind_nodes()/find_instrs()) to do something with every one of that kind, wherever it appears. 4. Reach forzuri.ast’s orzuri.compile’s own doc comments only for the parts exploration doesn’t answer on its own: what a node’s position covers, how aClosureinstruction’s resolved value nests a whole other instruction list inside it, and so on.
zuri.reflect doesn’t need any of this: it’s ordinary named functions
(function_info(f), has_method(object, name), …) with ordinary
parameters, not a generic tagged-record system, so its own doc comments
are the whole story; see zuri.reflect.
import zuri
for t in zuri.tokenize('var x = 1') {
echo t.kind
}
for node in zuri.parse('var x = 1') {
echo node.kind
}
for instr in zuri.compile('var x = 1') {
echo instr.op
}
def add(a, b) {
return a + b
}
echo zuri.reflect.function_info(add).arity
The zuri API
Every public name in zuri, wherever it is declared. Each links to the
page that documents it.
| Name | Kind | Summary |
|---|---|---|
zuri.Diagnostic | class | One problem in a piece of Zuri source: what is wrong and exactly where. |
zuri.Instr | class | One VM instruction. |
zuri.Node | class | One AST node: an expression, statement, declaration, type hint, or a standalone comment/doc block sitting… |
zuri.ParseResult | class | What parse_partial() read from a piece of source: the tree it built and every syntax error it found on the… |
zuri.Token | class | One lexical token from a Zuri source file. |
zuri.check | function | Every problem compiling source would report, as a list of zuri.Diagnostic in the order they occur,… |
zuri.check_file | function | Reads the file at path and checks it; exactly check(file(path).read()). |
zuri.compile | function | Compiles source as a standalone script and returns its instructions as a flat list of Instr. |
zuri.compile_file | function | Reads the file at path and compiles its contents; exactly compile(file(path).read()), for the common case… |
zuri.doc.Attached | class | One node from a list of siblings, with the doc block written directly above it, as attach() pairs them. |
zuri.doc.Block | class | A parsed doc block: body, its markdown prose as a list of lines, and tags, every @tag in the order it… |
zuri.doc.Tag | class | One @tag from a doc block, its wrapped lines already joined into one string. |
zuri.doc.attach | function | Pairs every node in a list of siblings with the doc block written directly above it. |
zuri.doc.empty | function | A block with neither prose nor tags, for a declaration that has no documentation. |
zuri.doc.module_block | function | The module’s own documentation among the top-level nodes of a file: the first doc block carrying an @module… |
zuri.doc.parse | function | Parses the text of a doc block, the text a DocBlock node carries, into its prose and tags. |
zuri.doc.parse_lines | function | Parses the lines of a doc block into its prose and tags; the same as parse() for text already split into… |
zuri.dump_file | function | Reads, parses, and dumps the file at path in one call: the fastest way to answer “what does this file’s AST… |
zuri.find_instrs | function | Every instruction reachable from instrs whose op is op, in the order they appear. |
zuri.find_nodes | function | Every node reachable from nodes whose kind is kind, in the order they appear. |
zuri.parse | function | Parses source in full and returns its top-level declarations as a list of Node. |
zuri.parse_file | function | Reads the file at path and parses its contents; exactly parse(file(path).read()), for the common case of… |
zuri.parse_partial | function | Parses source as far as it can and returns everything it read along with every syntax error it found,… |
zuri.parse_partial_file | function | Reads the file at path and parses as much of it as it can; exactly parse_partial(file(path).read()). |
zuri.reflect.bind_method | function | Method name on object’s class, bound to object itself as the receiver; the result can be called… |
zuri.reflect.class_info | function | Metadata for a class: { name, superclass_name, methods, fields, statics }. |
zuri.reflect.del_prop | function | Resets object’s existing property name back to nil. |
zuri.reflect.function_info | function | Metadata for a function-like value: { name, arity, variadic, is_method, owning_class_name, source_path }. |
zuri.reflect.gc | function | Runs a full garbage collection now, instead of waiting for the heap to cross its threshold. |
zuri.reflect.get_decorator | function | The decorator function named name (excluding the leading @) on the class behind object, bound to… |
zuri.reflect.get_method | function | The raw (unbound) closure for method name on the class behind object, or nil if it declares no such… |
zuri.reflect.get_prop | function | The current value of object’s (an instance or a module) property/member named name, or nil if it has… |
zuri.reflect.get_props | function | Every property/member name object (an instance or a module) has, as a list of strings, or an empty list if… |
zuri.reflect.has_decorator | function | Does the class behind object implement the decorator named name? A decorator is just a method whose own… |
zuri.reflect.has_method | function | Does the class behind object (an instance, or a class used directly) declare or inherit a method named… |
zuri.reflect.has_prop | function | Does object (an instance or a module) have a property/member named name? |
zuri.reflect.info | function | Dispatches to function_info/class_info/module_info based on kind(value). |
zuri.reflect.kind | function | This value’s runtime type tag: 'nil', 'bool', 'number', 'string', 'bytes', 'bigint', 'list',… |
zuri.reflect.module_info | function | Metadata for a module: { name, path, loaded, members }. |
zuri.reflect.pointer_type | function | The registered resource tag a pointer value was allocated with (e.g. a native socket handle’s own internal… |
zuri.reflect.set_prop | function | Overwrites object’s existing property name with value. |
zuri.tokenize | function | Lexes source in full and returns every token it contains, in source order, as a list of Token. |
zuri.tokenize_file | function | Reads the file at path and lexes its contents; exactly tokenize(file(path).read()), for the common case… |
zuri.walk_instrs | function | Recursively visits every instruction reachable from instrs (typically zuri.compile()’s own result, or any… |
zuri.walk_nodes | function | Recursively visits every node reachable from nodes (typically zuri.parse()’s own result, or any single… |
Submodules
| Module | Reached as | Summary |
|---|---|---|
zuri.ast | zuri.* | The Node type zuri.parse() returns: a generic representation of one AST node, tagged by kind… |
zuri.compile | zuri.* | The Instr type zuri.compile() returns: a generic representation of one VM instruction, tagged by op… |
zuri.diagnostic | zuri.* | The Diagnostic type: one problem found in a piece of Zuri source by reading it rather than running it, as… |
zuri.doc | zuri.doc.* | Reads Zuri’s documentation comments: the doc blocks a zuri.parse() tree keeps as DocBlock nodes, turned… |
zuri.reflect | zuri.reflect.* | Runtime introspection: metadata about a live function, class, or module, and property/method access on an… |
zuri.token | zuri.* | The Token type zuri.tokenize() returns: one entry per lexical token the lexer found in a source file, in… |
2026, Richard Ore and Zuri contributors
zuri.ast
import zuri
Everything here is re-exported by
zuri, soimport zuriis enough and the names are called aszuri.*. Importingzuri.aston its own works too and reaches the same definitions.
The Node type zuri.parse() returns: a generic
representation of one AST node, tagged by kind ('Binary', 'If',
'Function', …) with a fields dict holding whatever that kind
carries. There is one Node class rather than one class per grammar
rule, since the grammar has roughly fifty
expression/statement/declaration shapes, and a dedicated class per shape
would mean fifty near-identical classes to maintain in lockstep with the
parser forever after. A fields entry that’s itself node-shaped (or a
list of node-shaped entries) is already wrapped into a Node
recursively, so you never need to call Node.from_dict() yourself.
import zuri
for node in zuri.parse('def add(a, b) { return a + b }') {
echo node.kind
}
# Function
The tree is the program as it was written. A for loop is a For node,
x += 1 is a CompoundAssign, and an interpolated string is an
Interpolation; none of them is rewritten into the simpler forms the
compiler turns them into.
Positions
Every node records the whole of the source it was read from:
| Attribute | Meaning |
|---|---|
start | The offset of its first character |
end | The offset just past its last character |
line, col | Where it starts, both counted from 1 |
end_line, end_col | Where it ends: the column just past its last character |
Offsets count characters, not bytes, and index the source the way
slicing does, so source[node.start, node.end] is exactly the node’s
own text. A node covers its delimiters: a string literal’s quotes, a
block’s braces, a call’s parentheses.
A statement or declaration starts at its keyword: var total = 1 starts
at var and def add(a, b) at def. A class member starts at its
first word, static included. In a list of declarations, var a = 1, b,
each Var spans from its name to the end of its value.
A node that names something through a name of its own also says where
that name is, in its fields:
| Kind | Field | Covers |
|---|---|---|
Function, Method, Class | name_span | The declared name |
Property, Var, Argument | name_span | The declared name |
Get, Set | name_span | The property name after the . |
For | key_span, value_span | Each loop variable |
Import | segments | Each part of the path |
A span field is a plain dict with the same six keys a node has, and a
For without a key has a key_span of nil. An import segment adds
text, the part as written: '.', '..' or a name.
A node the source leaves implicit has an empty span, start == end,
where it would have been written: the nil of a return with no value
or of a var with no initializer sits just after the keyword or name,
and so does the Any type of an untyped parameter and the List type
of a ... parameter.
Comments and doc blocks
Every #-comment and doc block comes back as its own
'Comment'/'DocBlock'-kind node, sitting exactly where it appeared:
as a sibling in the surrounding statement or declaration list, at the
top level, inside a { } block, or between two class members.
A comment written in the middle of a statement, among a call’s arguments
or between an if block’s } and its else, has no place among that
statement’s own nodes. It comes back as a sibling placed immediately
after the smallest statement, declaration or class member that contains
it, at that statement’s depth: a comment inside an if body three
blocks deep comes back three blocks deep, never hoisted to the top. Its
position still says where it was written.
import zuri
var source = "def foo() {\n echo bar(a, # x\n b)\n echo 2\n}\n"
var body = zuri.parse(source)[0].fields.body.fields.statements
echo body[0].kind # Echo, the statement the comment sat inside
echo body[1].kind # Comment, placed right after it
echo body[1].fields.text # ' x'
echo body[2].kind # Echo, the "echo 2" line, unaffected
A comment above a declaration comes back right before it:
import zuri
var source = "# a helper function\ndef add(a, b) {\n return a + b\n}\n"
var nodes = zuri.parse(source)
echo nodes[0].kind # Comment
echo nodes[0].fields.text # " a helper function"
echo nodes[1].kind # Function
A class body’s fields and methods are two separate lists, a Class
node’s properties and methods, so a comment between a field and a
method lands in methods, the list of the member right after it, and a
comment between two fields lands in properties. A comment after the
last member goes to the list of the member before it. To rebuild a class
body in its written order, merge the two lists and sort them by start.
Node kinds
Expressions:
| Kind | Fields |
|---|---|
Nil, Self, Parent | None |
Bool, Integer, Float, BigNumber | value |
Literal | value: a string’s text, or a dictionary key written as a name |
Interpolation | parts: a string for each run of text and a node for each ${...} |
Identifier | name |
Unary | op, operand |
Binary | left, op, right, for the arithmetic, bitwise and shift operators |
Logical | left, op, right, for the comparisons |
Circuit | left, op, right, for and and or |
Condition | condition, then, otherwise: a ? b : c |
Grouping | inner: an expression in parentheses |
Range | lower, upper |
Call | callee, arguments |
Get | object, name, name_span |
Set | object, name, name_span, value: object.name = value |
Index | object, index |
Slice | object, lower, upper; an omitted bound is an implicit Nil |
List | items |
Dict | keys, values, in matching order |
Shorthand | name: the value of a { name } entry, which reads name |
Assign | target, value: target = value |
CompoundAssign | target, op, value: target += value and its kin |
Update | target, op: target++ or target-- |
Anonymous | declaration: a Function named @anon and a number |
TypeHint | types, nullable |
Argument | name, name_span, type_hint |
A Literal’s value leaves out the quotes, and an Interpolation’s
text parts come with their escapes already applied. A CompoundAssign’s
op and an Update’s name the operator written, such as 'PlusEq' or
'Increment'.
Each entry of a TypeHint’s types is a node named after the type:
Any, Bool, Int, Number, BigInt, String, Bytes, List,
Dict, Range, File, Function, Type, Callable, Iterable, or
Instance for a class. Every one carries class_name, the class an
Instance names and nil on the rest.
Statements:
| Kind | Fields |
|---|---|
Expression, Echo, Raise, Return | value |
Var | name, name_span, value, type_hint, is_constant |
VarList | declarations: the Var nodes of var a = 1, b = 2 |
Block | statements |
If | condition, then, otherwise |
While | condition, body |
DoWhile | body, condition |
For | key, key_span, value, value_span, iterable, body |
Iter | initializer, condition, steps, body |
Using | subject, arms, default_body |
When | labels, body: one arm of a Using |
Catch | body, catch_body, error_var |
Assert | condition, message |
Import | path, segments, name, aliased, elements, imports_all, exported |
Continue, Break | None |
Decl | declaration: a function declared inside a block |
Comment, DocBlock | text: the comment without its #, /* and */ |
A For written as for value in iterable has a key of nil. An
Iter clause left empty is nil, and with no steps steps is an empty
list.
Declarations:
| Kind | Fields |
|---|---|
Stmt | statement: a statement at the top level |
Function | name, name_span, parameters, body, is_variadic |
Class | name, name_span, superclass, properties, methods, is_extension |
Property | name, name_span, value, type_hint, is_static, is_constant |
Method | name, name_span, parameters, body, is_variadic, is_static |
An Import’s name is the Identifier it binds: the name after as
when aliased is true, otherwise the last part of its path, placed on
that part. path joins every part with the platform’s path separator. A
decorated method’s name keeps its @, as in '@new'.
Functions
parse()
zuri.parse(source) -> list[Node]
Parses source in full and returns its top-level declarations as a list
of Node. See this module’s own doc comment for the exact
shape, and in particular for what is and isn’t preserved around
comments.
Raises (rather than returning a partial result) on a syntax error, since there is no meaningful AST to hand back for source the parser itself couldn’t make sense of.
Parameters
source(string) — The Zuri source to parse
Returns list[Node]
Raises Error if source has a syntax error
parse_partial()
zuri.parse_partial(source: string) -> ParseResult
Parses source as far as it can and returns everything it read along
with every syntax error it found, without raising.
At a statement it cannot read, the parser reports the problem, skips to
where the next statement starts and carries on, so one mistake is
reported once and the rest of the source still comes back. The statement
that failed comes back as far as it was read, or as a None node when
nothing of it could be kept. A name the source is missing, such as the
variable name in var = 2, comes back as an empty string with an empty
span where the name belongs.
import zuri
var result = zuri.parse_partial('var a = 1\nvar = 2\nvar c = 3')
echo result.errors[0] # 2:5: Variable name expected.
echo result.nodes.length() # 3
Parameters
source(string) — The Zuri source to parse
Returns ParseResult
parse_partial_file()
zuri.parse_partial_file(path: string) -> ParseResult
Reads the file at path and parses as much of it as it can; exactly
parse_partial(file(path).read()).
Parameters
path(string) — Path to the Zuri source file to parse
Returns ParseResult
Raises Error if path can’t be opened or read
parse_file()
zuri.parse_file(path) -> list[Node]
Reads the file at path and parses its contents; exactly
parse(file(path).read()), for the common case of parsing a real file
rather than a source string you’ve already got in hand.
Parameters
path(string) — Path to the Zuri source file to parse
Returns list[Node]
Raises Error if path can’t be opened or read, or has a syntax
error
dump_file()
zuri.dump_file(path) -> string
Reads, parses, and dumps the file at path in one call: the fastest way
to answer “what does this file’s AST actually look like”. Equivalent to
parsing path and joining every resulting top-level node’s own dump()
with a blank line between them.
import zuri
echo zuri.dump_file('main.zu')
Parameters
path(string) — Path to the Zuri source file to parse and dump
Returns string
Raises Error if path can’t be opened or read, or has a syntax
error
walk_nodes()
zuri.walk_nodes(nodes, visitor)
Recursively visits every node reachable from nodes (typically
zuri.parse()’s own result, or any single Node plucked out
of it), in the order they appear, calling visitor for each one. This
is the tool for “do something with every X”: most real uses of an AST
are a walk, not manual field-by-field navigation.
visitor is either a plain function, called with every node
(visitor(node)), or a dict from kind name to function, where only a
node of that kind gets called (visitor[node.kind](node)). Reach for
the dict form when you only care about one or two kinds; it reads as a
small dispatch table instead of a chain of if node.kind == ... checks
inside a single function.
Always visits the whole tree; there’s no way for visitor to skip
descending into a particular node’s children.
import zuri
var nodes = zuri.parse(source)
# every function name declared anywhere in the file, including
# nested/inner functions
var names = []
zuri.walk_nodes(nodes, {
Function: @(node) { names.append(node.fields.name) },
})
Parameters
nodes(Node|list[Node])visitor(function|dict)
find_nodes()
zuri.find_nodes(nodes, kind) -> list[Node]
Every node reachable from nodes whose kind is kind, in the order
they appear. Shorthand for the common “just give me a flat list of
these” case walk_nodes() itself is built on.
import zuri
var nodes = zuri.parse(source)
for comment in zuri.find_nodes(nodes, 'Comment') {
echo comment.fields.text
}
Parameters
nodes(Node|list[Node])kind(string)
Returns list[Node]
Classes
Node
class zuri.Node
One AST node: an expression, statement, declaration, type hint, or a standalone comment/doc block sitting between two of those.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
zuri.Node(kind: string, position: dict, fields: dict)
Returns a new instance of a Node directly, from
already-wrapped field values. Ordinary Zuri code should use
Node.from_dict() or zuri.parse() instead, since this constructor
does no wrapping of its own.
Parameters
kind(string) — The node’s kind, e.g.'Binary','If'position(dict) — Where the node sits in its source, as{ start, end, line, col, end_line, end_col }; see this module’s own docs for what each one countsfields(dict) — This kind’s own fields, already wrapped
Node.from_dict()
zuri.Node.from_dict(data: dict) -> Node
Builds a Node from a dict holding a node’s parts ({ kind, start, end, line, col, end_line, end_col, fields }),
recursively wrapping any node-shaped entry inside fields, a single
nested node or a list of them, into its own Node. A fields
entry that isn’t node-shaped (a string, a number, a bool, nil, or a
plain dict such as a name_span) passes through unchanged.
Parameters
data(dict) —{ kind, start, end, line, col, end_line, end_col, fields }
Returns Node
Node.is_trivia()
zuri.Node.is_trivia() -> bool
Is this node a standalone comment or doc block (as opposed to a real expression/statement/declaration)? See this module’s own doc comment for exactly where these can and can’t appear.
Returns bool
Node.to_string()
zuri.Node.to_string() -> string
This node as kind@line:col, e.g. 'Binary@3:5', naming the line and
column it starts at. A one-line summary; use dump() to actually see
what’s inside a node.
Returns string
Node.dump()
zuri.Node.dump() -> string
A readable, indented, multi-line dump of this node and everything inside
it, recursively. This is the actual way to find out what a fields
entry is called for a kind you haven’t looked up yet: echo node.dump()
and read it, rather than guessing a key name and checking whether it
happens to exist.
import zuri
var nodes = zuri.parse('def add(a, b) { return a + b }')
echo nodes[0].dump()
prints
Function@1:1
name: 'add'
name_span: 1:5-1:8
parameters:
Argument@1:9
name: 'a'
name_span: 1:9-1:10
type_hint:
TypeHint@1:10
types:
Any@1:10
class_name: nil
nullable: true
Argument@1:12
name: 'b'
name_span: 1:12-1:13
type_hint:
TypeHint@1:13
types:
Any@1:13
class_name: nil
nullable: true
body:
Block@1:15
statements:
Return@1:17
value:
Binary@1:24
left:
Identifier@1:24
name: 'a'
op: 'Plus'
right:
Identifier@1:28
name: 'b'
is_variadic: false
Each node’s header is its kind and the line and column it starts at. A
span field such as name_span prints as the range it covers,
line:col-end_line:end_col. The untyped parameters carry an implicit
Any type sitting just after their names, and class_name is nil on
Any because only an Instance type names a class.
Returns string
ParseResult
class zuri.ParseResult
What parse_partial() read from a piece of source: the tree it built
and every syntax error it found on the way.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
zuri.ParseResult(nodes: list, errors: list)
Returns a new ParseResult. Ordinary code receives
these from parse_partial() rather than building them.
Parameters
nodes(list) — The top-level nodes read, asparse()returns themerrors(list) — Every syntax error, as {zuri.Diagnostic}s, in the order they occur in the source
ParseResult.is_clean()
zuri.ParseResult.is_clean() -> bool
True when the source parsed without a single error, in which case
nodes is exactly what parse() returns.
Returns bool
ParseResult.to_string()
zuri.ParseResult.to_string() -> string
This result as ParseResult(n nodes, m errors).
Returns string
2026, Richard Ore and Zuri contributors
zuri.compile
import zuri
Everything here is re-exported by
zuri, soimport zuriis enough and the names are called aszuri.*. Importingzuri.compileon its own works too and reaches the same definitions.
The Instr type zuri.compile() returns: a generic
representation of one VM instruction, tagged by op ('LoadConst',
'Add', 'Jmp', …) with a fields dict holding that opcode’s own
operands. As with zuri.ast.Node, there’s one
Instr class rather than one class per opcode. The instruction set has
around sixty opcodes, and a dedicated class per opcode would be sixty
near-identical classes to keep in sync with the compiler forever after.
import zuri
for instr in zuri.compile('var x = 1 + 2') {
echo '${instr.op} ${instr.fields}'
}
Resolved operands
compile() returns a flat instruction list, matching the VM’s own
Instr type, but several opcodes (LoadConst, GetGlobal, GetField,
Invoke, …) only carry a raw index into the compiler’s own constant
pool, which isn’t otherwise exposed. Rather than force every caller to
also separately fetch and correlate a constants table, each such field
gets its already-resolved value embedded right on the instruction too,
alongside the raw index: e.g. a LoadConst node’s fields has both
const_idx (the raw index) and value (what it actually points at). A
resolved field is never literally named after the raw index’s own
subject when that word is a reserved keyword (const, class, …),
since a dict key can’t be read back with plain dot-access syntax then;
class becomes class_reg (it’s a register index anyway, not a
resolved class value), and similarly for a handful of others.
A Closure instruction’s resolved value is a nested function prototype
({ name, arity, variadic, instructions }), whose own instructions is
recursively wrapped the exact same way, so a script with functions or
methods in it exposes their bodies too, not just the code immediately
around them.
Every instruction node carries the source line it was compiled from,
but never a column. The compiler’s own line tracking is line-only
(statement granularity), unlike a
zuri.token.Token’s or
zuri.ast.Node’s position, which both have real
column info from the lexer/parser.
Functions
walk_instrs()
zuri.walk_instrs(instrs, visitor)
Recursively visits every instruction reachable from instrs (typically
zuri.compile()’s own result, or any single Instr plucked
out of it), including inside a Closure instruction’s own nested
function body, in the order they appear, calling visitor for each one.
visitor is either a plain function, called with every instruction
(visitor(instr)), or a dict from opcode name to function, where only a
matching instruction gets called (visitor[instr.op](instr)). Reach for
the dict form when you only care about one or two opcodes.
Always visits the whole tree; there’s no way for visitor to skip
descending into a nested function body.
import zuri
var instrs = zuri.compile(source)
# every global name this script writes to, anywhere, including
# inside nested function bodies
var written = []
zuri.walk_instrs(instrs, {
SetGlobal: @(instr) { written.append(instr.fields.name) },
})
Parameters
instrs(Instr|list[Instr])visitor(function|dict)
find_instrs()
zuri.find_instrs(instrs, op) -> list[Instr]
Every instruction reachable from instrs whose op is op, in the
order they appear. Shorthand for the common “just give me a flat list of
these” case walk_instrs() itself is built on.
import zuri
var instrs = zuri.compile(source)
for jmp in zuri.find_instrs(instrs, 'Jmp') {
echo jmp.fields.offset
}
Parameters
instrs(Instr|list[Instr])op(string)
Returns list[Instr]
compile()
zuri.compile(source) -> list[Instr]
Compiles source as a standalone script and returns its instructions as
a flat list of Instr. See this module’s own doc comment for
exactly what each instruction carries.
Raises (rather than returning partial bytecode) on a lexer, parser, or compiler error.
Parameters
source(string) — The Zuri source to compile
Returns list[Instr]
Raises Error if source doesn’t compile
compile_file()
zuri.compile_file(path) -> list[Instr]
Reads the file at path and compiles its contents; exactly
compile(file(path).read()), for the common case of compiling a real
file rather than a source string you’ve already got in hand.
Parameters
path(string) — Path to the Zuri source file to compile
Returns list[Instr]
Raises Error if path can’t be opened or read, or doesn’t compile
check()
zuri.check(source: string) -> list[Diagnostic]
Every problem compiling source would report, as a list of
zuri.Diagnostic in the order they
occur, without running anything and without raising. An empty list means
source compiles.
Source that does not parse reports its syntax errors, exactly as
parse_partial() finds them. Source that parses goes on to the
compiler, which reports a name declared twice in one scope, self or
parent outside a method, parent in a class with no superclass,
break or continue outside a loop, an assignment to a constant, a
private member reached through anything other than self or parent, a
class extension declaring a field or an instance method, and a function
too large for its registers.
import zuri
for problem in zuri.check('if true {\n break\n}') {
echo problem # 2:3: 'break' used outside of a loop
}
echo zuri.check('var x = 1').length() # 0
Parameters
source(string) — The Zuri source to check
Returns list[Diagnostic]
check_file()
zuri.check_file(path: string) -> list[Diagnostic]
Reads the file at path and checks it; exactly
check(file(path).read()).
Parameters
path(string) — Path to the Zuri source file to check
Returns list[Diagnostic]
Raises Error if path can’t be opened or read
Classes
Instr
class zuri.Instr
One VM instruction.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
zuri.Instr(op, line, fields)
Returns a new instance of an Instr directly, from already-
wrapped field values. Ordinary Zuri code should use zuri.compile()
instead; this constructor does no wrapping of its own.
Parameters
op(string) — The opcode name, e.g.'LoadConst','Add'line(?number) — The source line this instruction was compiled from, ornilif the compiler never stamped onefields(dict) — This opcode’s own operands, already wrapped
Instr.to_string()
zuri.Instr.to_string() -> string
This instruction as op@line, e.g. 'LoadConst@3', or just op when
it carries no line. A one-line summary; use dump() to actually see
what’s inside an instruction.
Returns string
Instr.dump()
zuri.Instr.dump() -> string
A readable, indented, multi-line dump of this instruction and everything
inside it, recursively (including a Closure instruction’s own nested
function body). Same idea as
zuri.ast.Node.dump: the actual way to find out
what an opcode’s fields looks like is to run something and read the
dump, not to guess a key name from the opcode’s name alone.
import zuri
var instrs = zuri.compile('var x = 1')
echo instrs[0].dump()
prints
LoadConst@0
dst: 0
const_idx: 1
value: 1
Returns string
2026, Richard Ore and Zuri contributors
zuri.diagnostic
import zuri
Everything here is re-exported by
zuri, soimport zuriis enough and the names are called aszuri.*. Importingzuri.diagnosticon its own works too and reaches the same definitions.
The Diagnostic type: one problem found in a piece of
Zuri source by reading it rather than running it, as
zuri.parse_partial() and zuri.check() reports them.
Classes
Diagnostic
class zuri.Diagnostic
One problem in a piece of Zuri source: what is wrong and exactly where.
The position has the same six parts a
zuri.ast.Node has. start and end are character
offsets, the first character the problem is about and the one just past
it, so source[d.start, d.end] is the text it is about. line and
col are where that text starts and end_line and end_col where it
ends, all counted from 1.
A syntax error covers the token it is about. When the source ends where more was expected, that is an empty span at the very end. A compiler error covers the token it is about, or the whole of its line when it belongs to the line rather than to one token.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
zuri.Diagnostic(message: string, position: dict)
Returns a new Diagnostic. Ordinary code receives these
from zuri.parse_partial() and zuri.check() rather than building
them.
Parameters
message(string) — What is wrong, without a positionposition(dict) —{ start, end, line, col, end_line, end_col }
Diagnostic.to_string()
zuri.Diagnostic.to_string() -> string
This diagnostic as line:col: message, e.g. '2:3: 'break' used outside of a loop'.
Returns string
2026, Richard Ore and Zuri contributors
zuri.doc
import zuri
zuriexposes this aszuri.doc, soimport zuriis enough and the names are called aszuri.doc.*.import zuri.docreaches the same definitions directly.
Reads Zuri’s documentation comments: the doc blocks a zuri.parse()
tree keeps as DocBlock nodes, turned into prose and a list of @tag
entries, and paired with the declarations they document.
A doc block is markdown prose followed by a run of tags. Each tag starts
a line with @ and its name, and its text may wrap onto the lines below
it, which are recognised by their indentation. A blank line ends the run
of tags.
import zuri
# Spelled in pieces so this example can sit inside a doc block.
var open = '/' + '**'
var close = '*' + '/'
var source = '${open}
* Adds two numbers.
*
* @param number a: The first number
* @returns number
${close}
def add(a, b) {
return a + b
}'
for attached in zuri.doc.attach(zuri.parse(source)) {
echo attached.node.fields.name # add
echo attached.block.summary() # Adds two numbers.
echo attached.block.first('param').name # a
}
Fenced code inside a block is kept as it is, with no line of it read as a tag, and since Zuri block comments nest, an example that opens a comment of its own does not end the block it sits in.
Several tags have a second spelling, read as the first: @return as
@returns, @raises as @throws, @params as @param, and @number
as @numeric.
Functions
parse_lines()
zuri.doc.parse_lines(lines: list) -> Block
Parses the lines of a doc block into its prose and tags; the same as
parse() for text already split into lines.
Everything before the first tag is prose. Once tags start, a line
beginning with whitespace continues the tag above it, a line beginning
with @ starts a new one, and a blank line ends the run.
Each line is read from just after the * that starts a line of a doc
block, dropping that * and the one space after it, so whatever
indentation follows is what marks a continuation. A line with no * is
read from its first character that is not whitespace, and so never
continues a tag.
Parameters
lines(list) — The block’s text, one string per line
Returns Block
parse()
zuri.doc.parse(text: string) -> Block
Parses the text of a doc block, the text a DocBlock node carries,
into its prose and tags.
import zuri
var block = zuri.doc.parse('Opens the file.\n\n@param string path\n@returns file')
echo block.summary() # Opens the file.
echo block.first('returns').type # file
Parameters
text(string) — The block’s text, as aDocBlocknode carries it
Returns Block
empty()
zuri.doc.empty() -> Block
A block with neither prose nor tags, for a declaration that has no documentation.
Returns Block
module_block()
zuri.doc.module_block(nodes: list) -> ?Block
The module’s own documentation among the top-level nodes of a file: the
first doc block carrying an @module tag, parsed, or nil when the
file has none.
Parameters
nodes(list) — The file’s top-level nodes, aszuri.parse()returns them
Returns ?Block
attach()
zuri.doc.attach(nodes: list) -> list[Attached]
Pairs every node in a list of siblings with the doc block written directly above it.
A doc block documents the node that follows it and nothing else:
anything in between, a plain # comment included, breaks the pairing.
The module’s own doc block, the first carrying an @module tag,
documents the module rather than the node after it, so it is paired with
nothing. Every node other than a doc block comes back, in order,
documented or not.
nodes is any list of siblings: a file’s top-level nodes, a block’s
statements, or a class’s properties or methods. A top-level
declaration that is a statement comes back as the Stmt node wrapping
it, exactly as it sits in the list.
Parameters
nodes(list) — A list of sibling nodes fromzuri.parse()
Returns list[Attached]
Classes
Tag
class zuri.doc.Tag
One @tag from a doc block, its wrapped lines already joined into one
string.
kind is the tag’s name without its @, under its main spelling: an
@return is a 'returns' tag. type is the type the tag names,
written as {type} or as a type word leading its text, and name is
the name an @param or @property tag names. Both are empty strings,
never nil, when the tag carries none. text is what remains: the
description.
import zuri
var block = zuri.doc.parse('@param {string} name: Who to greet')
var tag = block.tags[0]
echo tag.kind # param
echo tag.type # string
echo tag.name # name
echo tag.text # Who to greet
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
zuri.doc.Tag(kind: string, type: string, name: string, text: string)
Returns a new Tag. Ordinary code receives these from parse()
rather than building them.
Parameters
kind(string) — The tag’s name without its@type(string) — The type it names, or''name(string) — The name it names, or''text(string) — Its description
Tag.to_string()
zuri.doc.Tag.to_string() -> string
This tag as Tag(@kind type name).
Returns string
Block
class zuri.doc.Block
A parsed doc block: body, its markdown prose as a list of lines, and
tags, every @tag in the order it was written.
Blank lines at the start and end of the prose are dropped. Each line
keeps the indentation it had after the * that starts it, so code
blocks and nested lists read as they were written.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
zuri.doc.Block(body: list, tags: list)
Returns a new Block. Ordinary code receives these from
parse() and attach() rather than building them.
Parameters
body(list) — The prose, one string per linetags(list) — The block’s {Tag}s, in source order
Block.all()
zuri.doc.Block.all(kind: string) -> list[Tag]
Every tag of one kind, in source order; an empty list when the block has
none. kind is a tag’s main spelling, such as 'returns'.
Parameters
kind(string)
Returns list[Tag]
Block.first()
zuri.doc.Block.first(kind: string) -> ?Tag
The first tag of one kind, or nil when the block has none.
Parameters
kind(string)
Returns ?Tag
Block.has()
zuri.doc.Block.has(kind: string) -> bool
True when the block carries at least one tag of this kind.
Parameters
kind(string)
Returns bool
Block.summary()
zuri.doc.Block.summary() -> string
The block’s first paragraph of prose as one line, for a one-line summary in an index. It stops at the first blank line, heading or code fence, and is an empty string for a block that opens with one of those.
Returns string
Block.is_empty()
zuri.doc.Block.is_empty() -> bool
True when the block has neither prose nor tags.
Returns bool
Block.to_string()
zuri.doc.Block.to_string() -> string
This block as Block(n lines, m tags).
Returns string
Attached
class zuri.doc.Attached
One node from a list of siblings, with the doc block written directly
above it, as attach() pairs them.
block is an empty Block when there is no doc block above
the node, and documented says which: a doc block that holds nothing
still counts as documenting its node.
- printable — has a
@to_string(), soechoandprint()show something useful
Constructor
zuri.doc.Attached(node, block: instance, documented: bool)
Returns a new Attached. Ordinary code receives these from
attach() rather than building them.
Parameters
node(Node) — The node, exactly as it sits in the listblock(Block) — Its documentationdocumented(bool) — Whether a doc block was written above it
Attached.to_string()
zuri.doc.Attached.to_string() -> string
This pairing as Attached(kind, documented).
Returns string
2026, Richard Ore and Zuri contributors
zuri.reflect
import zuri
zuriexposes this aszuri.reflect, soimport zuriis enough and the names are called aszuri.reflect.*.import zuri.reflectreaches the same definitions directly.
Runtime introspection: metadata about a live function, class, or module, and property/method access on an instance or module by name, without needing to already know that name at compile time. It also runs the garbage collector on demand.
This is deliberately a RUNTIME-only view. It does not know a
function/class’s declaration line or its doc block; those live in
source, not in the running object, and are a zuri.parse()/zuri AST
concern instead (walk the parsed source and read a Function/Class
node’s own line/col, and any DocBlock node sitting right before
it).
import zuri
def add(a, b) {
return a + b
}
echo zuri.reflect.function_info(add).arity
# 2
Functions
kind()
zuri.reflect.kind(value) -> string
This value’s runtime type tag: 'nil', 'bool', 'number',
'string', 'bytes', 'bigint', 'list', 'dict', 'function',
'class', 'instance', 'module', 'range', 'file', or a pointer’s
own registered resource tag (see pointer_type). A closure, a native
function, and a bound method are all reported as 'function', since
from Zuri’s own point of view they’re interchangeably callable.
Parameters
value(any)
Returns string
pointer_type()
zuri.reflect.pointer_type(value) -> ?string
The registered resource tag a pointer value was allocated with (e.g. a
native socket handle’s own internal type name), or nil if value
isn’t a pointer at all. Pointers wrap an opaque native resource that
Zuri code can’t otherwise inspect; this names what kind of resource one
is without exposing its contents.
Parameters
value(any)
Returns ?string
function_info()
zuri.reflect.function_info(f) -> dict
Metadata for a function-like value: { name, arity, variadic, is_method, owning_class_name, source_path }.
owning_class_name is the class it was declared a method of, or nil
for an ordinary function/closure. source_path is nil for a native
(built-in) function, which has no Zuri source file behind it.
Parameters
f(callable) — A closure, a native function, or a bound method
Returns dict
Raises Error if f isn’t callable as a function (a class, though
itself callable to construct an instance, is a class_info subject
instead)
class_info()
zuri.reflect.class_info(c) -> dict
Metadata for a class: { name, superclass_name, methods, fields, statics }.
superclass_name is nil for a class with no superclass. methods is
a dict from method name to that method’s own function_info-shaped
entry, own and inherited pre-merged (an override shadows the inherited
entry of the same name, matching how method dispatch itself works).
fields and statics are plain lists of names, ordered by declaration;
fields includes inherited fields, statics is own-only (statics are
never inherited in this runtime).
Parameters
c(class)
Returns dict
Raises Error if c isn’t a class
module_info()
zuri.reflect.module_info(m) -> dict
Metadata for a module: { name, path, loaded, members }. members is a
dict from every top-level name the module declares to that binding’s own
kind (see kind above), e.g. { add: 'function', VERSION: 'string' }.
Accepts either a raw module value or a promoted import PATH binding;
both describe the same underlying module.
Parameters
m(module)
Returns dict
Raises Error if m isn’t a module
info()
zuri.reflect.info(value) -> dict
Dispatches to function_info/class_info/module_info based on
kind(value).
Parameters
value(any)
Returns dict
Raises Error if value has no reflectable shape (a number, a
list, an instance, …); reflect describes callables, classes, and
modules only
has_prop()
zuri.reflect.has_prop(object, name) -> bool
Does object (an instance or a module) have a property/member named
name?
Parameters
object(instance|module)name(string)
Returns bool
get_prop()
zuri.reflect.get_prop(object, name) -> any
The current value of object’s (an instance or a module)
property/member named name, or nil if it has none by that name.
Parameters
object(instance|module)name(string)
Returns any
Raises TypeError if object is neither an instance nor a module
get_props()
zuri.reflect.get_props(object) -> list[string]
Every property/member name object (an instance or a module) has, as a
list of strings, or an empty list if it has none. Instance fields are
ordered by declaration; module members have no such inherent order.
Parameters
object(instance|module)
Returns list[string]
Raises TypeError if object is neither an instance nor a module
set_prop()
zuri.reflect.set_prop(object, name, value)
Overwrites object’s existing property name with value.
Parameters
object(instance)name(string)
Returns — bool: true if the property was updated, false if
name isn’t a field object’s class declares (nothing is changed in
that case; this never creates a new field)
Raises TypeError if object isn’t an instance
Note: instances in this runtime have a fixed set of fields, sized once when their class is declared. Unlike a dict, a property can never be added to or removed from an instance at runtime. This only ever updates a field the instance’s class already declares; it can’t create a new one.
del_prop()
zuri.reflect.del_prop(object, name)
Resets object’s existing property name back to nil.
Parameters
object(instance)name(string)
Returns — bool: true if the property was reset, false if name
isn’t a field object’s class declares
Raises TypeError if object isn’t an instance
Note: as with
set_prop, this can’t remove the field itself, only clear its value; seeset_prop’s own note.
has_method()
zuri.reflect.has_method(object, name) -> bool
Does the class behind object (an instance, or a class used directly)
declare or inherit a method named name?
Parameters
object(instance|class)name(string)
Returns bool
has_decorator()
zuri.reflect.has_decorator(object, name) -> bool
Does the class behind object implement the decorator named name? A
decorator is just a method whose own name starts with @ (@to_string,
@new, @value, …); name here excludes that leading @.
Parameters
object(instance|class)name(string) — The decorator’s name, without the leading@
Returns bool
get_method()
zuri.reflect.get_method(object, name) -> ?function
The raw (unbound) closure for method name on the class behind
object, or nil if it declares no such method. Calling the result
directly does NOT supply a receiver; see bind_method for a version
that does.
Parameters
object(instance|class)name(string)
Returns ?function
bind_method()
zuri.reflect.bind_method(object, name) -> ?function
Method name on object’s class, bound to object itself as the
receiver; the result can be called directly with no receiver argument,
unlike get_method’s raw closure.
Parameters
object(instance)name(string)
Returns ?function
get_decorator()
zuri.reflect.get_decorator(object, name) -> function
The decorator function named name (excluding the leading @) on the
class behind object, bound to object as its receiver and ready to
call directly.
Parameters
object(instance)name(string) — The decorator’s name, without the leading@
Returns function
Raises NotImplementedError if the class behind object implements
no such decorator
gc()
zuri.reflect.gc() -> nil
Runs a full garbage collection now, instead of waiting for the heap to cross its threshold.
Every object nothing reaches is freed before this returns, and anything
that releases a resource when collected releases it: an owned ffi
pointer’s destructor runs, for one. The memory freed goes back to the
system before this returns too.
A program never needs this to stay correct; the collector runs on its own as the program allocates. It is for the moments the timing matters: releasing native resources at a known point, or a test that checks what collection does. A full collection visits every live object, so calling it in a loop is slow.
import zuri
var scratch = [1, 2, 3]
scratch = nil
zuri.reflect.gc()
Returns nil
2026, Richard Ore and Zuri contributors
zuri.token
import zuri
Everything here is re-exported by
zuri, soimport zuriis enough and the names are called aszuri.*. Importingzuri.tokenon its own works too and reaches the same definitions.
The Token type zuri.tokenize() returns: one entry per
lexical token the lexer found in a source file, in source order, with
nothing filtered out. Comments, doc blocks, and newlines are all
ordinary tokens here, exactly as the lexer itself produces them. The
parser’s own grammar quietly skips those when it runs, but tokenize()
reports everything the lexer actually saw.
import zuri
for t in zuri.tokenize('var x = 1 + 2') {
echo '${t.kind} ${t.line}:${t.column} ${t.text}'
}
Functions
tokenize()
zuri.tokenize(source) -> list[Token]
Lexes source in full and returns every token it contains, in source
order, as a list of Token.
Nothing is filtered: Comment, DocBlock, and Newline tokens are all
included, and the list always ends with one Eof token.
A lexer-level problem in source, an unterminated string, an unbalanced
doc block, an unexpected character, does not raise; it surfaces as an
ordinary token with kind 'Error' instead, so this function is total
over any input, including malformed source.
Parameters
source(string) — The Zuri source to lex
Returns list[Token]
tokenize_file()
zuri.tokenize_file(path) -> list[Token]
Reads the file at path and lexes its contents; exactly
tokenize(file(path).read()), for the common case of lexing a real file
rather than a source string you’ve already got in hand.
Parameters
path(string) — Path to the Zuri source file to lex
Returns list[Token]
Raises Error if path can’t be opened or read
Classes
Token
class zuri.Token
One lexical token from a Zuri source file.
Every instance has:
-
kind(string): the token’s kind, e.g.'Identifier','Plus','Comment','Eof', the exact variant name from the lexer’s own token kind enum. -
line/column(number): 1-indexed source position this token starts at;columncounts characters, not bytes. -
start/end(number): character offsets into the source string spanning this token’s exact text. -
text(string): this token’s exact source text,source[start, end], delimiters included: a string literal’s quotes, a doc block’s opening and closing markers, and so on. -
value(any): this token’s own payload, where it has one, a literal, identifier, comment, or doc block’s string content, a number, or a bigint.nilfor every fixed symbol or keyword, and forNewline/Eof. AnError-kind token instead carries{ message, line, offset }describing what went wrong. -
printable — has a
@to_string(), soechoandprint()show something useful
Constructor
zuri.Token(data)
Returns a new instance of a Token. Ordinary Zuri code never
needs to call this directly, since zuri.tokenize() builds these for
you.
Parameters
data(dict) —{ kind, line, column, start, end, text, value }, the token’s fields
Token.is_trivia()
zuri.Token.is_trivia() -> bool
Is this token a comment or a doc block? Neither ever reaches the
parser’s own grammar, but both are real entries in zuri.tokenize()’s
output.
Returns bool
Token.to_string()
zuri.Token.to_string() -> string
This token as kind@line:column, e.g. 'Identifier@1:5'.
Returns string
2026, Richard Ore and Zuri contributors