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.