Skip to content

API reference🔗

The public API is exactly what sil_lift re-exports at the top level; underscore-prefixed modules are implementation details.

Read, write, and validate LIFT (Lexicon Interchange Format) 0.13 lexicons.

The public API is exactly what this module re-exports; underscore-prefixed modules are implementation details.

LiftError 🔗

Bases: Exception

Base class for all sil-lift errors.

LiftParseError 🔗

Bases: LiftError

A document could not be read as LIFT 0.13.

Raised for non-XML input, a non-LIFT root element, or a LIFT version other than 0.13 (sil-lift does not migrate; see the lift-standard repo's XSLTs for one-off migration of legacy files).

LiftValidationError 🔗

Bases: LiftError

Raised by the fail-fast validation wrappers on the first error-level :class:~sil_lift.Problem (warnings never raise).

Extras dataclass 🔗

Out-of-schema content carried losslessly (opaque; see module docstring).

to_string() 🔗

Human-readable dump of the carried content (not a serialization format).

FieldDefinition dataclass 🔗

A header <field tag=...>: documents a field type used in the document.

Header dataclass 🔗

The optional <header>: description, ranges, field definitions.

ranges_extra / fields_extra carry out-of-schema LIFT residue found on the <ranges> / <fields> wrapper elements themselves (unknown attributes, stray nodes). Those wrappers have no model object of their own, so their residue is kept apart from the header's and re-emitted onto the wrapper rather than migrating up onto <header>.

Range dataclass 🔗

A <range>: a controlled vocabulary, inline and/or via href.

href points at an external .lift-ranges resource; its contents are carried unresolved here.

add_element(id, *, parent=None, guid=None) 🔗

Append a new :class:RangeElement to this range and return it.

The label / abbrev / description multitexts start empty; populate them on the returned element (e.g. el.label["en"] = "...").

RangeElement dataclass 🔗

A <range-element>: one value in a range; parent builds hierarchies.

Changes dataclass 🔗

Everything that differs between a lexicon and the document it was loaded from.

Falsy only when :meth:Lexicon.save would reproduce the source bytes, which makes it the correct guard for skipping an in-place write — :meth:Lexicon.changed_entries alone is not, since it covers only entry content.

The guarantee is one-way. Every condition that sends the writer down its canonical path is reported here, so a falsy result is never a wrong "nothing to write"; but that path can land back on the source bytes for a document already in canonical form, so a truthy result means the write is not provably unnecessary rather than certainly needed.

reordered answers only where the same entries are still present. An addition or a removal re-serializes the document on its own, so a reordering alongside one is not reported separately — enough for the guard, short of a full account of what happened.

baseline is False when no byte snapshot was captured. Nothing can be compared then, so the fields say what :meth:Lexicon.save will write rather than what differs: entries lists every entry and header is True if there is a header to emit, while added, removed, and root stay empty or False — nothing is known to have been there before. The object is truthy either way.

Entry dataclass 🔗

Bases: _Extensible

An <entry>. A set date_deleted marks it deleted (a tombstone).

gloss_langs() 🔗

Every language that has a gloss in any sense or subsense.

Etymology dataclass 🔗

Bases: _Extensible

An <etymology type=... source=...>; each gloss carries its own lang.

Example dataclass 🔗

Bases: _Extensible

An <example> under a sense.

Field dataclass 🔗

Bases: _ExtensibleNoFields

An entry-level usage <field type=...> (extensible, but no nested field).

GrammaticalInfo dataclass 🔗

A <grammatical-info value=...>; the extensibility exception — traits only, no fields.

Lexicon 🔗

The root handle: a parsed .lift document and its folder companions.

load(path, *, resolve_ranges=True) classmethod 🔗

Parse a .lift file (LIFT 0.13 only) into a full object graph.

With resolve_ranges (the default), companion .lift-ranges files are loaded and tracked in :attr:ranges_files. Several candidates are tried and every one that exists is loaded: the conventional <name>.lift-ranges sibling, and for each header range/@href both the href resolved as a path relative to the .lift file and its bare basename in the same directory (FLEx hrefs are usually dangling absolute file://C:/... paths from the exporting machine, so the basename is what resolves locally).

A .zip path is treated as a packaged LIFT folder: it is extracted to a temporary directory (kept alive for the returned lexicon's lifetime) and the single contained .lift is loaded.

save(path=None) 🔗

Write the .lift file and every tracked .lift-ranges companion.

Untouched entries are emitted byte-identical to the source; modified entries are re-serialized canonically with all residue preserved. With no path, saves to where the lexicon was loaded from. When saving into a different directory, companions are written next to the new .lift file under their original basenames. Saving under a new name in the same directory leaves companions at their original paths (they are shared with the original document, not copied).

Raises :class:ValueError if no target path is available (none was passed and the lexicon was not loaded from a file).

save_zip(path, *, wrap_folder=True) 🔗

Write the lexicon and its folder companions as a zip package.

The .lift and .lift-ranges are (re-)serialized with the usual fidelity (untouched entries byte-identical); any other files from the source package (media, WritingSystems/, consent/, ...) are carried through verbatim. wrap_folder controls the layout: True (default) nests everything under a folder named after the zip — the convention FieldWorks and The Combine expect on import — False writes the files at the archive root, and a string uses that folder name. The archive container itself is not byte-reproducible.

sort() 🔗

Sort into canonical order, in place: entries by (guid, id), header ranges/range-elements by id, field definitions by tag.

Sorting alone does not mark entries as modified — a subsequent :meth:save still emits untouched entries byte-identically, just in the new order. For fully re-serialized diff-ready output use :func:sil_lift.canonicalize.

changed_entries() 🔗

Entries currently in the lexicon whose content differs from the document as loaded.

An entry's digest covers its whole subtree, so an edit at any depth — a gloss on a nested subsense included — reports the containing entry. Assigning a field the value it already had reports nothing, and neither does reordering (see :meth:sort).

The comparison is always against the document as loaded, never against the most recent :meth:save, so an entry stays reported once changed.

Content changes only. Entries added since loading are reported by :meth:added_entries and removed ones by :meth:removed_entries; for everything that makes :meth:save differ from the source — including the header, the ranges companions, and the root element — use :meth:changes. An empty result here does not mean the document would round-trip byte-identically.

One report per entry, not per occurrence: an entry aliased into the list twice is named once here, so a count of this list is a count of entries. Against a baseline the repeat is an addition (see :meth:added_entries), which accounts for every occurrence exactly once; without a baseline, nothing is reported as added, so the repeat goes unnamed even though :meth:save writes it.

Every entry is reported when there is no byte baseline to compare against — a lexicon built from scratch, and equally one whose source the byte scanner declined to read (an encoding that is not ASCII-compatible, or a scanner/parser disagreement). Both re-serialize in full on :meth:save, so those entries genuinely are rewritten.

Costs one canonical serialization pass over the entries.

added_entries() 🔗

Entries in the lexicon that were not in the loaded document.

An entry the document already held, appended a second time, counts as an addition too: the list gained an occurrence, and :meth:save writes the entry out twice.

Empty when there is no byte baseline: nothing is known to be new, since nothing is known to have been there before. Needs no serialization.

removed_entries() 🔗

Entries from the loaded document that are no longer in the lexicon.

The entry objects survive because the parse-time records hold them, so a removed entry is returned intact rather than merely counted.

The mirror of the addition rule: an entry aliased into the list twice is removed only once no occurrence is left. Dropping just one leaves the list reordered, or unchanged if what went was the repeat.

Empty when there is no byte baseline. Needs no serialization.

changes() 🔗

Everything that differs between this lexicon and the document it was loaded from.

Falsy only when :meth:save would reproduce the source bytes, which makes it the correct guard for skipping an in-place write::

if not lex.changes():
    return

One-way, and in the safe direction — see :class:Changes.

Content only, so it says nothing about the destination: a save(path) into another directory writes the document and its companions there whatever this reports, and skipping that is a lost copy, not a saved one. Never guard :meth:save_zip with it — that has no in-place form, its archive is never byte-reproducible, and it carries package files (media, WritingSystems/, ...) that nothing here inspects.

The most expensive query here: it serializes the entries, the header, and every tracked companion's ranges. When only one part is wanted, :meth:changed_entries, :meth:added_entries, and :meth:removed_entries answer individually, and the latter two need no serialization at all.

It is not a shortcut around :meth:save — it costs more than the save it guards. Deciding which source bytes can be reused digests every entry again, and nothing is cached between the two calls, so guarding a write that turns out to be needed roughly doubles the work. What the guard buys is not writing: an unchanged file-modification time, no spurious diff, nothing downstream woken up.

iter_problems(*, require_ids=False) 🔗

Validate the in-memory state (schema layers + semantic checks).

The schema layers need serialized bytes: what :meth:save would write is validated, so in-memory edits are always visible. For an untouched loaded document those are the source bytes (line numbers match the file on disk); otherwise serialization is a documented cost on large lexicons.

With require_ids, entries missing a guid and senses missing an id are reported as missing-id errors — stricter than LIFT (both are optional there), for workflows that re-import by a stable id.

all_ranges() 🔗

Inline and external ranges, merged by id.

An inline header range that enumerates its own elements wins; otherwise the external definition (from any tracked ranges file) is used. External ranges never referenced by the header are included too.

add_ranges_file(ranges_file=None, *, href) 🔗

Attach a companion .lift-ranges document so :meth:save writes it.

For every range already in ranges_file that the header does not list, a <range id=... href=...> reference is added to the header so LIFT consumers can find the companion. href is the reference as written in the header — normally a filename beside the .lift (e.g. "mydict.lift-ranges"); the companion is written next to the saved .lift under that basename. Populate the companion before calling (or call again to reference ranges added later).

Returns the attached (or newly created) :class:RangesFile.

media_refs() 🔗

Every <media> and <illustration> reference, with its owner.

missing_media() 🔗

Media references whose files don't exist in the LIFT folder layout.

A relative href is checked as given (backslashes normalized) and under the conventional subfolder (audio/ for media, pictures/ for illustrations). Remote/absolute hrefs can't be checked and are skipped.

find(*, id=None, guid=None) 🔗

The first entry matching the given id and/or guid, or None.

Raises :class:ValueError if neither id nor guid is given.

MediaRef dataclass 🔗

One media reference in the document, with its owner's identity.

Note dataclass 🔗

Bases: _Extensible

A <note>; sibling notes conventionally have distinct types.

Pronunciation dataclass 🔗

Bases: _Extensible

A <pronunciation> with optional <media> references.

RangesChanges dataclass 🔗

What differs between a companion .lift-ranges and the file it was read from.

baseline is False when no byte snapshot was captured (a companion built from scratch, or a source the byte scanner declined to read). Nothing can be compared then, so the fields say what :meth:RangesFile.save will write rather than what differs: ranges lists everything, while added, removed, and root stay empty or False — nothing is known to have been there before. The object is truthy either way: the file will be written in full.

ranges names each range once, even one aliased into the list twice; against a baseline the repeat is reported by added, and without one it is not reported at all, though :meth:RangesFile.save still writes it. reordered answers only where the same ranges are still present, as on :class:Changes.

RangesFile 🔗

A standalone .lift-ranges document (root <lift-ranges>).

save(path=None) 🔗

Write the .lift-ranges file (byte-identical when unchanged).

Raises :class:ValueError if no target path is available (none was passed and the file was not loaded from disk).

add_range(id, *, href=None, guid=None) 🔗

Append a new :class:Range to this file and return it.

Populate its values with :meth:Range.add_element.

sort() 🔗

Sort ranges and their elements into canonical (id) order.

changes() 🔗

Everything that differs between this companion and the file it was read from.

Falsy only when :meth:save would reproduce the source bytes, one-way in the same safe direction as :class:Changes. Costs one canonical serialization pass over the ranges.

Relation dataclass 🔗

Bases: _Extensible

A <relation type=... ref=...> cross-reference to another entry/sense.

Reversal dataclass 🔗

A <reversal> index entry (not extensible).

ReversalMain dataclass 🔗

A reversal <main>: the parent node in a reversal-index chain (recursive).

Sense dataclass 🔗

Bases: _Extensible

A <sense> (or <subsense> — same content model, recursive).

gloss(lang) 🔗

The gloss text in lang, or None (first match; each <gloss> has its own lang).

Translation dataclass 🔗

A <translation> of an example (not extensible).

URLRef dataclass 🔗

A <media> or <illustration>: an href with an optional label.

Variant dataclass 🔗

Bases: _Extensible

A <variant>: an inline variant form and/or a ref cross-reference.

LiftReader 🔗

Lazy entry iterator over a .lift file; the header is parsed eagerly.

Use as a context manager. The version guard runs at open time; the header (which precedes entries in a conforming file) is available immediately as :attr:header.

LiftWriter 🔗

Streaming writer: header up front, then one canonical chunk per entry.

Use as a context manager; the closing </lift> is written only on a clean exit (an exception leaves an unterminated file, deliberately — a half-written lexicon must not look complete).

Annotation dataclass 🔗

An <annotation>: reviewer/editorial metadata on a node.

Form dataclass 🔗

A single-language text unit: <form lang=...><text/></form>, also <gloss>.

lang is required by the schema; it is None only when reading schema-invalid real-world files (which sil-lift loads rather than rejects).

Multitext dataclass 🔗

An insertion-ordered collection of forms, one per language.

Behaves like a Mapping[str, Text] keyed by language (mt["en"]), with assignment coercing plain strings (mt["en"] = "dog"). The underlying forms list is the full truth — forms with a None lang (schema-invalid input) are reachable there but not via mapping keys.

Span dataclass 🔗

An inline markup run inside a <text>; nests recursively.

Text dataclass 🔗

Mixed content of a <text>: ordered str and Span fragments.

__str__() 🔗

Plain-text flattening; span markup is stripped, span text kept.

Trait dataclass 🔗

A <trait>: a name/value pair, typically keyed to a range.

Problem dataclass 🔗

One validation finding, addressable to a file/entry/line.

canonicalize(src, dst) 🔗

Write a fully canonical copy of a .lift file: sorted entries and ranges, documented child grouping and attribute order, 2-space layout.

Unlike :meth:Lexicon.save, the output is entirely re-serialized (no byte-preserving passthrough) — that is the point: two canonicalized files diff cleanly. Text content is never whitespace-normalized. The whole document is held in memory (sorting requires it; the C# oracle buffers too).

Only the .lift file is written: companion .lift-ranges files are neither read nor rewritten (the source is loaded with resolve_ranges=False). Sort a ranges file separately via :meth:RangesFile.sort + :meth:RangesFile.save. This differs from :meth:Lexicon.save, which writes every tracked companion.

open_reader(path) 🔗

Open a .lift file for streaming reads (bounded memory).

open_writer(path, *, header=None, producer=None, ranges=None) 🔗

Open a .lift file for streaming writes (bounded memory).

If ranges is given, its companion .lift-ranges is written beside path on clean close, and matching <range href> references are added to header (created if absent) so the document points to the companion.

iter_problems(path, *, require_ids=False) 🔗

All problems in the document and its tracked companions, lazily.

With require_ids, also report entries/senses missing a stable id (missing-id errors); see :meth:Lexicon.iter_problems.

validate_file(path) 🔗

Raise :class:LiftValidationError on the first error-level problem.

load(path, *, resolve_ranges=True) 🔗

Parse a .lift file (LIFT 0.13 only) into a :class:Lexicon.

Companion .lift-ranges files are located and loaded too unless resolve_ranges=False; see :meth:Lexicon.load.