eee-project 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eee_project/__init__.py +460 -0
- eee_project/_chain.py +86 -0
- eee_project/_exceptions.py +46 -0
- eee_project/_grammar_fmt.py +103 -0
- eee_project/_hooks.py +28 -0
- eee_project/_protocol.py +49 -0
- eee_project/_registry.py +246 -0
- eee_project/_results.py +40 -0
- eee_project/_slot_template.py +50 -0
- eee_project/_tag_registry.py +55 -0
- eee_project/backends/__init__.py +0 -0
- eee_project/backends/_mg_features.py +158 -0
- eee_project/backends/modern_greek.py +122 -0
- eee_project/backends/unimorph.py +255 -0
- eee_project/backends/unimorph_tags.py +36 -0
- eee_project/data/__init__.py +0 -0
- eee_project/data/labels/__init__.py +0 -0
- eee_project/data/labels/adj-el.tsv +41 -0
- eee_project/data/labels/adj-en.tsv +41 -0
- eee_project/data/labels/adj-ru.tsv +41 -0
- eee_project/data/labels/noun-el.tsv +41 -0
- eee_project/data/labels/noun-en.tsv +41 -0
- eee_project/data/labels/noun-ru.tsv +41 -0
- eee_project/data/labels/tense-el.tsv +15 -0
- eee_project/data/labels/tense-en.tsv +15 -0
- eee_project/data/labels/tense-ru.tsv +15 -0
- eee_project/data/labels/ui-el.tsv +35 -0
- eee_project/data/labels/ui-en.tsv +35 -0
- eee_project/data/labels/ui-ru.tsv +35 -0
- eee_project/data/labels/verb-el.tsv +193 -0
- eee_project/data/labels/verb-en.tsv +193 -0
- eee_project/data/labels/verb-ru.tsv +193 -0
- eee_project/data/languages.yaml +45 -0
- eee_project/data/unimorph/ell.tsv +211923 -0
- eee_project/data/unimorph/grc.tsv +44026 -0
- eee_project/notebook_utils.py +5686 -0
- eee_project-1.0.0.dist-info/METADATA +161 -0
- eee_project-1.0.0.dist-info/RECORD +42 -0
- eee_project-1.0.0.dist-info/WHEEL +4 -0
- eee_project-1.0.0.dist-info/licenses/AUTHORS +1 -0
- eee_project-1.0.0.dist-info/licenses/LICENSE +73 -0
- eee_project-1.0.0.dist-info/licenses/NOTICE +27 -0
eee_project/__init__.py
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""eee — language-agnostic morphology umbrella for the EEE project."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from eee_project import _registry
|
|
5
|
+
from eee_project._registry import set_chain, get_chain, get_chain_entry
|
|
6
|
+
from eee_project._exceptions import (
|
|
7
|
+
AmbiguousPOSError,
|
|
8
|
+
BackendLoadError,
|
|
9
|
+
FeatureNotSupportedError,
|
|
10
|
+
PosNotSupportedError,
|
|
11
|
+
UnsupportedLanguageError,
|
|
12
|
+
)
|
|
13
|
+
from eee_project._results import InflectResult, LemmaEntry, AnalysisEntry
|
|
14
|
+
from eee_project._hooks import HookContext, PreHook, PostHook
|
|
15
|
+
from eee_project._slot_template import SlotTemplate, SupportsSlotTemplates
|
|
16
|
+
from eee_project._tag_registry import register_tag_type
|
|
17
|
+
from eee_project._grammar_fmt import fmt_ud_feats
|
|
18
|
+
from eee_project.notebook_utils import (
|
|
19
|
+
GreekUtils, GreekConfig, MODERN_GREEK, ANCIENT_GREEK,
|
|
20
|
+
eee_topbar, eee_footer, magnify_image, strip_diacritics, greek_compare,
|
|
21
|
+
load_ga_config, ConfigStore,
|
|
22
|
+
build_grc_paradigm_table, build_modern_paradigm_table, build_grc_lexicon_tabs,
|
|
23
|
+
make_paradigm_form, interactive_text, setup_ancient_greek, add_labels,
|
|
24
|
+
filter_grc_quiz_words, grc_coverage_words, grc_lexicon_sources, norm_grc_surface, resolve_clicked_word,
|
|
25
|
+
parse_stanza_text, parse_stanza_translations,
|
|
26
|
+
LEXICON_TAG_POS, LEXICON_TAG_POS_ALIASES, TRANSLATION_PRESENCE_CONTENT_POS,
|
|
27
|
+
_INC as increment_counter,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = "1.0.0"
|
|
31
|
+
|
|
32
|
+
_UNSET = object() # sentinel distinguishing "not provided" from explicit None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _eff_hook(hook, default):
|
|
36
|
+
"""Resolve an _UNSET hook sentinel: _UNSET → default, explicit value → that value."""
|
|
37
|
+
return default if hook is _UNSET else hook
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _validate_dispatch_args(backend: str | None, chain: list | None) -> None:
|
|
41
|
+
if backend is not None and chain is not None:
|
|
42
|
+
raise ValueError("backend= and chain= are mutually exclusive")
|
|
43
|
+
if chain is not None and not chain:
|
|
44
|
+
raise ValueError("chain= must not be empty")
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"inflect",
|
|
48
|
+
"list_lemmas",
|
|
49
|
+
"register_backend",
|
|
50
|
+
"set_fallback_backend",
|
|
51
|
+
"supported_languages",
|
|
52
|
+
"language_info",
|
|
53
|
+
"UnsupportedLanguageError",
|
|
54
|
+
"BackendLoadError",
|
|
55
|
+
"AmbiguousPOSError",
|
|
56
|
+
"PosNotSupportedError",
|
|
57
|
+
"FeatureNotSupportedError",
|
|
58
|
+
"InflectResult",
|
|
59
|
+
"LemmaEntry",
|
|
60
|
+
"AnalysisEntry",
|
|
61
|
+
"analyze",
|
|
62
|
+
"analyze_traced",
|
|
63
|
+
"HookContext",
|
|
64
|
+
"PreHook",
|
|
65
|
+
"PostHook",
|
|
66
|
+
"set_chain",
|
|
67
|
+
"get_chain",
|
|
68
|
+
"inflect_traced",
|
|
69
|
+
"list_lemmas_traced",
|
|
70
|
+
"SlotTemplate",
|
|
71
|
+
"SupportsSlotTemplates",
|
|
72
|
+
"register_tag_type",
|
|
73
|
+
"get_slot_templates",
|
|
74
|
+
"inflect_slot",
|
|
75
|
+
"GreekUtils",
|
|
76
|
+
"GreekConfig",
|
|
77
|
+
"MODERN_GREEK",
|
|
78
|
+
"ANCIENT_GREEK",
|
|
79
|
+
"eee_topbar",
|
|
80
|
+
"eee_footer",
|
|
81
|
+
"magnify_image",
|
|
82
|
+
"strip_diacritics",
|
|
83
|
+
"greek_compare",
|
|
84
|
+
"fmt_ud_feats",
|
|
85
|
+
"load_ga_config",
|
|
86
|
+
"ConfigStore",
|
|
87
|
+
"build_grc_paradigm_table",
|
|
88
|
+
"build_modern_paradigm_table",
|
|
89
|
+
"build_grc_lexicon_tabs",
|
|
90
|
+
"make_paradigm_form",
|
|
91
|
+
"interactive_text",
|
|
92
|
+
"setup_ancient_greek",
|
|
93
|
+
"add_labels",
|
|
94
|
+
"filter_grc_quiz_words",
|
|
95
|
+
"grc_coverage_words",
|
|
96
|
+
"grc_lexicon_sources",
|
|
97
|
+
"norm_grc_surface",
|
|
98
|
+
"resolve_clicked_word",
|
|
99
|
+
"parse_stanza_text",
|
|
100
|
+
"parse_stanza_translations",
|
|
101
|
+
"increment_counter",
|
|
102
|
+
"LEXICON_TAG_POS",
|
|
103
|
+
"LEXICON_TAG_POS_ALIASES",
|
|
104
|
+
"TRANSLATION_PRESENCE_CONTENT_POS",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _resolve_chain_args(
|
|
109
|
+
lang: str,
|
|
110
|
+
chain: list[str] | None,
|
|
111
|
+
pre_hook,
|
|
112
|
+
post_hook,
|
|
113
|
+
) -> tuple[list[str], object, object]:
|
|
114
|
+
"""Return (effective_backends, effective_pre, effective_post) for chain dispatch.
|
|
115
|
+
|
|
116
|
+
_UNSET means the caller did not pass a hook — inherit the registered chain hook.
|
|
117
|
+
None means the caller explicitly wants no hook — suppress any registered hook.
|
|
118
|
+
"""
|
|
119
|
+
if chain is not None:
|
|
120
|
+
return chain, _eff_hook(pre_hook, None), _eff_hook(post_hook, None)
|
|
121
|
+
entry = get_chain_entry(lang) or {}
|
|
122
|
+
return (entry.get("backends", []),
|
|
123
|
+
_eff_hook(pre_hook, entry.get("pre_hook")),
|
|
124
|
+
_eff_hook(post_hook, entry.get("post_hook")))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _run_single_backend(
|
|
128
|
+
lang: str, key: str, backend_instance: object,
|
|
129
|
+
lemma: str, features: "dict[str, str] | str", pos: str,
|
|
130
|
+
pre_hook, post_hook,
|
|
131
|
+
) -> set[str]:
|
|
132
|
+
"""Apply pre/post hooks around one backend.inflect() call.
|
|
133
|
+
|
|
134
|
+
Shared by the named-backend path (_run_named_backend) and the no-chain
|
|
135
|
+
default-backend path in inflect_traced() — both call exactly one backend,
|
|
136
|
+
unlike BackendChain which iterates a list and silently skips backends
|
|
137
|
+
that fail to load or raise (wrong here: an explicit backend= or the
|
|
138
|
+
resolved default backend must propagate failures, not be swallowed).
|
|
139
|
+
"""
|
|
140
|
+
_pre = _eff_hook(pre_hook, None)
|
|
141
|
+
_post = _eff_hook(post_hook, None)
|
|
142
|
+
_lemma, _features, _pos = lemma, features, pos
|
|
143
|
+
if _pre is not None:
|
|
144
|
+
_ctx = HookContext(lemma=_lemma, features=_features, pos=_pos,
|
|
145
|
+
language=lang, tried=[], stop="first")
|
|
146
|
+
_lemma, _features, _pos = _pre(_lemma, _features, _pos, _ctx)
|
|
147
|
+
result = set(backend_instance.inflect(_lemma, _features, _pos, language=lang))
|
|
148
|
+
if _post is not None:
|
|
149
|
+
_ctx = HookContext(lemma=_lemma, features=_features, pos=_pos,
|
|
150
|
+
language=lang, tried=[key], stop="first")
|
|
151
|
+
result = set(_post(result, _ctx))
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _run_named_backend(
|
|
156
|
+
lang: str, backend: str, lemma: str, features: "dict[str, str] | str", pos: str,
|
|
157
|
+
pre_hook, post_hook,
|
|
158
|
+
) -> tuple[set[str], str]:
|
|
159
|
+
"""Run a single named backend with optional pre/post hooks. Returns (result, key)."""
|
|
160
|
+
key = f"{lang}:{backend}"
|
|
161
|
+
result = _run_single_backend(
|
|
162
|
+
lang, key, _registry.get_backend(lang, backend=backend),
|
|
163
|
+
lemma, features, pos, pre_hook, post_hook,
|
|
164
|
+
)
|
|
165
|
+
return result, key
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def inflect(
|
|
169
|
+
lemma: str,
|
|
170
|
+
features: "dict[str, str] | str",
|
|
171
|
+
pos: str,
|
|
172
|
+
language: str | None = None,
|
|
173
|
+
backend: str | None = None,
|
|
174
|
+
chain: list[str] | None = None,
|
|
175
|
+
pre_hook=_UNSET,
|
|
176
|
+
post_hook=_UNSET,
|
|
177
|
+
) -> set[str]:
|
|
178
|
+
"""Return inflected forms for lemma matching the given UD feature bundle.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
lemma: base form of the word
|
|
183
|
+
features: UD FEATS dict, e.g. {"Tense": "Pres", "Mood": "Ind", "Person": "1"},
|
|
184
|
+
or a raw backend tag string (e.g. "N;NOM;SG") for direct index lookup.
|
|
185
|
+
When str, all backends in the chain receive the str unchanged; backends
|
|
186
|
+
that do not handle str will raise. Use inflect_slot() for raw-tag dispatch.
|
|
187
|
+
pos: part of speech — "verb", "noun", "adjective", "adverb"
|
|
188
|
+
language: IETF language tag — "el", "grc", etc. Always required.
|
|
189
|
+
backend: named backend variant, e.g. "unimorph". None selects the default.
|
|
190
|
+
Mutually exclusive with chain=.
|
|
191
|
+
chain: per-call ordered list of backend short names. Overrides the registered
|
|
192
|
+
chain for this call only. Mutually exclusive with backend=.
|
|
193
|
+
pre_hook: callable(lemma, features, pos, ctx) -> (lemma, features, pos), or None
|
|
194
|
+
to suppress any registered chain hook for this call.
|
|
195
|
+
post_hook: callable(forms, ctx) -> set[str], or None to suppress.
|
|
196
|
+
|
|
197
|
+
Raises
|
|
198
|
+
------
|
|
199
|
+
ValueError if both backend= and chain= are provided, or chain=[].
|
|
200
|
+
UnsupportedLanguageError if language cannot be resolved or no backend found
|
|
201
|
+
BackendLoadError if a backend is found but fails to load
|
|
202
|
+
"""
|
|
203
|
+
return inflect_traced(
|
|
204
|
+
lemma, features, pos, language=language, backend=backend, chain=chain,
|
|
205
|
+
pre_hook=pre_hook, post_hook=post_hook,
|
|
206
|
+
).forms
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def inflect_traced(
|
|
210
|
+
lemma: str,
|
|
211
|
+
features: "dict[str, str] | str",
|
|
212
|
+
pos: str,
|
|
213
|
+
language: str | None = None,
|
|
214
|
+
backend: str | None = None,
|
|
215
|
+
chain: list[str] | None = None,
|
|
216
|
+
stop: str = "first",
|
|
217
|
+
pre_hook=_UNSET,
|
|
218
|
+
post_hook=_UNSET,
|
|
219
|
+
) -> InflectResult:
|
|
220
|
+
"""Return inflected forms with full chain attribution metadata.
|
|
221
|
+
|
|
222
|
+
Unlike inflect(), always returns an InflectResult with .forms, .source, and
|
|
223
|
+
.tried. When backend= is given, treats it as a one-element chain so the result
|
|
224
|
+
carries correct source/tried attribution. backend= and chain= are mutually
|
|
225
|
+
exclusive.
|
|
226
|
+
"""
|
|
227
|
+
_validate_dispatch_args(backend, chain)
|
|
228
|
+
|
|
229
|
+
lang = _registry.resolve_language(language, backend)
|
|
230
|
+
|
|
231
|
+
if backend is not None:
|
|
232
|
+
result, key = _run_named_backend(lang, backend, lemma, features, pos, pre_hook, post_hook)
|
|
233
|
+
source = key if result else None
|
|
234
|
+
return InflectResult(forms=result, source=source, tried=[key], by_backend={key: set(result)})
|
|
235
|
+
|
|
236
|
+
effective_backends, effective_pre, effective_post = _resolve_chain_args(
|
|
237
|
+
lang, chain, pre_hook, post_hook
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if effective_backends:
|
|
241
|
+
from eee_project._chain import BackendChain
|
|
242
|
+
return BackendChain(
|
|
243
|
+
language=lang, backends=effective_backends, stop=stop,
|
|
244
|
+
pre_hook=effective_pre, post_hook=effective_post,
|
|
245
|
+
).run(lemma, features, pos)
|
|
246
|
+
|
|
247
|
+
# No-chain single-backend path — apply per-call hooks if provided
|
|
248
|
+
backend_key = f"{lang}:default"
|
|
249
|
+
result = _run_single_backend(
|
|
250
|
+
lang, backend_key, _registry.get_backend(lang),
|
|
251
|
+
lemma, features, pos, effective_pre, effective_post,
|
|
252
|
+
)
|
|
253
|
+
source = backend_key if result else None
|
|
254
|
+
return InflectResult(forms=result, source=source, tried=[backend_key], by_backend={backend_key: set(result)})
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _chain_walk_traced(lang: str, backend: str | None, method_name: str, arg, entry_ctor):
|
|
258
|
+
"""Query method_name(arg) on each backend in the chain (or an explicit backend=).
|
|
259
|
+
|
|
260
|
+
Shared by list_lemmas_traced() and analyze_traced() -- same three-branch
|
|
261
|
+
shape (explicit backend= / no chain registered / chain loop), differing
|
|
262
|
+
only in which backend method to call, what argument it takes, and how to
|
|
263
|
+
wrap each raw result item. entry_ctor(item, source_key) builds one entry.
|
|
264
|
+
Backends without method_name are silently skipped. Duplicates across
|
|
265
|
+
backends are NOT collapsed.
|
|
266
|
+
"""
|
|
267
|
+
if backend is not None:
|
|
268
|
+
b = _registry.get_backend(lang, backend=backend)
|
|
269
|
+
fn = getattr(b, method_name, None)
|
|
270
|
+
if fn is None:
|
|
271
|
+
return []
|
|
272
|
+
key = f"{lang}:{backend}"
|
|
273
|
+
return [entry_ctor(item, key) for item in fn(arg)]
|
|
274
|
+
|
|
275
|
+
chain = _registry.get_chain(lang)
|
|
276
|
+
if not chain:
|
|
277
|
+
b = _registry.get_backend(lang)
|
|
278
|
+
fn = getattr(b, method_name, None)
|
|
279
|
+
if fn is None:
|
|
280
|
+
return []
|
|
281
|
+
return [entry_ctor(item, lang) for item in fn(arg)]
|
|
282
|
+
|
|
283
|
+
result = []
|
|
284
|
+
for name in chain:
|
|
285
|
+
try:
|
|
286
|
+
b = _registry.get_backend(lang, backend=name)
|
|
287
|
+
except (UnsupportedLanguageError, BackendLoadError):
|
|
288
|
+
continue
|
|
289
|
+
fn = getattr(b, method_name, None)
|
|
290
|
+
if fn is None:
|
|
291
|
+
continue
|
|
292
|
+
key = f"{lang}:{name}"
|
|
293
|
+
result.extend(entry_ctor(item, key) for item in fn(arg))
|
|
294
|
+
return result
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def list_lemmas_traced(
|
|
298
|
+
pos: str,
|
|
299
|
+
language: str,
|
|
300
|
+
backend: str | None = None,
|
|
301
|
+
) -> list[LemmaEntry]:
|
|
302
|
+
"""Return lemmas with per-entry source attribution.
|
|
303
|
+
|
|
304
|
+
Queries each backend in the registered chain (or explicit backend=). Returns
|
|
305
|
+
one LemmaEntry per (lemma, backend) pair — duplicates across backends are NOT
|
|
306
|
+
collapsed. Backends without list_lemmas() are silently skipped.
|
|
307
|
+
"""
|
|
308
|
+
lang = _registry.resolve_language(language, backend)
|
|
309
|
+
return _chain_walk_traced(lang, backend, "list_lemmas", pos,
|
|
310
|
+
lambda lm, key: LemmaEntry(lemma=lm, source=key))
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def list_lemmas(pos: str, language: str | None = None, backend: str | None = None) -> list[str]:
|
|
314
|
+
"""Return lemmas available in the backend's corpus for the given POS.
|
|
315
|
+
|
|
316
|
+
When backend=None and a chain is registered for language, queries all chain
|
|
317
|
+
backends and returns a deduplicated union. Returns [] for algorithm-based
|
|
318
|
+
backends that have no finite vocabulary.
|
|
319
|
+
"""
|
|
320
|
+
lang = _registry.resolve_language(language, backend)
|
|
321
|
+
|
|
322
|
+
if backend is None and _registry.get_chain(lang):
|
|
323
|
+
return list(dict.fromkeys(e.lemma for e in list_lemmas_traced(pos, lang)))
|
|
324
|
+
|
|
325
|
+
b = _registry.get_backend(lang, backend=backend)
|
|
326
|
+
fn = getattr(b, "list_lemmas", None)
|
|
327
|
+
if fn is None:
|
|
328
|
+
return []
|
|
329
|
+
return fn(pos)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def analyze_traced(
|
|
333
|
+
form: str,
|
|
334
|
+
language: str,
|
|
335
|
+
backend: str | None = None,
|
|
336
|
+
) -> list[AnalysisEntry]:
|
|
337
|
+
"""Return candidate reverse-lookup analyses for form with per-entry source attribution.
|
|
338
|
+
|
|
339
|
+
Queries each backend in the registered chain (or explicit backend=). Returns
|
|
340
|
+
one AnalysisEntry per (candidate, backend) pair — duplicates across backends
|
|
341
|
+
are NOT collapsed. Backends without analyze() are silently skipped.
|
|
342
|
+
"""
|
|
343
|
+
lang = _registry.resolve_language(language, backend)
|
|
344
|
+
return _chain_walk_traced(lang, backend, "analyze", form,
|
|
345
|
+
lambda r, key: AnalysisEntry(**r, source=key))
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def analyze(form: str, language: str | None = None, backend: str | None = None) -> list[dict]:
|
|
349
|
+
"""Return candidate reverse-lookup analyses for form as plain dicts, deduplicated.
|
|
350
|
+
|
|
351
|
+
When backend=None and a chain is registered for language, queries all chain
|
|
352
|
+
backends and returns a deduplicated union (by lemma+pos+tag). Returns [] for
|
|
353
|
+
backends without analyze().
|
|
354
|
+
"""
|
|
355
|
+
lang = _registry.resolve_language(language, backend)
|
|
356
|
+
|
|
357
|
+
if backend is None and _registry.get_chain(lang):
|
|
358
|
+
dedup: dict[tuple, dict] = {}
|
|
359
|
+
for e in analyze_traced(form, lang):
|
|
360
|
+
dedup.setdefault((e.lemma, e.pos, e.tag),
|
|
361
|
+
{"lemma": e.lemma, "pos": e.pos, "tag": e.tag, "features": e.features})
|
|
362
|
+
return list(dedup.values())
|
|
363
|
+
|
|
364
|
+
b = _registry.get_backend(lang, backend=backend)
|
|
365
|
+
fn = getattr(b, "analyze", None)
|
|
366
|
+
if fn is None:
|
|
367
|
+
return []
|
|
368
|
+
return fn(form)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def get_slot_templates(
|
|
372
|
+
lang: str, pos: str, terms_lang: str = "en", *, backend: "str | None" = None
|
|
373
|
+
) -> "list[SlotTemplate] | None":
|
|
374
|
+
"""Return slot templates for (lang, pos, terms_lang) from the registered backend.
|
|
375
|
+
|
|
376
|
+
Uses the default backend only; does not walk chains. Returns None if no
|
|
377
|
+
backend is registered for lang, the backend has no get_slot_templates
|
|
378
|
+
attribute, or the backend returns None for this combination.
|
|
379
|
+
|
|
380
|
+
Pass backend='name' to select a named variant (e.g. 'ancient-greek' vs
|
|
381
|
+
'unimorph' when both are registered for the same language).
|
|
382
|
+
"""
|
|
383
|
+
resolved = _registry.resolve_language(lang, backend)
|
|
384
|
+
try:
|
|
385
|
+
_backend = _registry.get_backend(resolved, backend)
|
|
386
|
+
except (UnsupportedLanguageError, BackendLoadError):
|
|
387
|
+
return None
|
|
388
|
+
fn = getattr(_backend, "get_slot_templates", None)
|
|
389
|
+
if fn is None:
|
|
390
|
+
return None
|
|
391
|
+
return fn(lang, pos, terms_lang)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def inflect_slot(
|
|
395
|
+
lemma: str,
|
|
396
|
+
slot: "SlotTemplate",
|
|
397
|
+
pos: str,
|
|
398
|
+
*,
|
|
399
|
+
language: str,
|
|
400
|
+
backend: "str | object | None" = None,
|
|
401
|
+
) -> set[str]:
|
|
402
|
+
"""Inflect lemma for a single SlotTemplate using the registered tag_type dispatch.
|
|
403
|
+
|
|
404
|
+
Bypasses chains and hooks — slot-based calls use the backend-native tag directly.
|
|
405
|
+
For UD slots needing chain semantics use eee.inflect(lemma, slot.features, pos, language=language).
|
|
406
|
+
|
|
407
|
+
Parameters
|
|
408
|
+
----------
|
|
409
|
+
backend : str | object | None
|
|
410
|
+
Named backend variant (str), explicit backend instance (object), or None to
|
|
411
|
+
use the default registered backend. Pass an instance when the language is not
|
|
412
|
+
registered with eee (e.g. non-bundled UniMorph languages loaded via
|
|
413
|
+
register_language()).
|
|
414
|
+
|
|
415
|
+
Raises
|
|
416
|
+
------
|
|
417
|
+
KeyError if slot.tag_type is not registered
|
|
418
|
+
UnsupportedLanguageError if no backend is found for language (and backend=None)
|
|
419
|
+
"""
|
|
420
|
+
from eee_project._tag_registry import _get_tag_dispatch
|
|
421
|
+
dispatch_fn = _get_tag_dispatch(slot.tag_type)
|
|
422
|
+
lang = _registry.resolve_language(language, None)
|
|
423
|
+
if isinstance(backend, str) or backend is None:
|
|
424
|
+
_backend = _registry.get_backend(lang, backend=backend)
|
|
425
|
+
else:
|
|
426
|
+
_backend = backend
|
|
427
|
+
return dispatch_fn(_backend, lemma, slot, pos, lang)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def register_backend(code: str, instance: object, backend: str | None = None) -> None:
|
|
431
|
+
"""Register or override a backend instance for a language code.
|
|
432
|
+
|
|
433
|
+
Pass backend='name' to register a named variant alongside the default.
|
|
434
|
+
Idempotent: calling twice with the same instance produces the same state.
|
|
435
|
+
Overrides any existing registration, including cached entry-point instances.
|
|
436
|
+
"""
|
|
437
|
+
_registry.register_backend(code, instance, backend=backend)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def set_fallback_backend(instance: object) -> None:
|
|
441
|
+
"""Register a catch-all backend for unregistered languages.
|
|
442
|
+
|
|
443
|
+
Not included in supported_languages().
|
|
444
|
+
"""
|
|
445
|
+
_registry.set_fallback_backend(instance)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def language_info(code: str) -> dict | None:
|
|
449
|
+
"""Return the manifest entry for the given EEE language code, or None if unknown."""
|
|
450
|
+
return _registry.language_info(code)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def supported_languages() -> dict[str, list[str]]:
|
|
454
|
+
"""Return a mapping of language code → list of entry point values for discovered backends.
|
|
455
|
+
|
|
456
|
+
Includes all entry-point backends without triggering lazy loads. Multiple backends
|
|
457
|
+
may register for the same language code (e.g. dedicated + unimorph); all are listed.
|
|
458
|
+
Does NOT include explicitly registered backends or the fallback backend.
|
|
459
|
+
"""
|
|
460
|
+
return _registry.supported_languages()
|
eee_project/_chain.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Backend chain executor."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from eee_project._exceptions import BackendLoadError, UnsupportedLanguageError
|
|
7
|
+
from eee_project._hooks import HookContext, PostHook, PreHook
|
|
8
|
+
from eee_project._registry import get_backend
|
|
9
|
+
from eee_project._results import InflectResult
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BackendChain:
|
|
15
|
+
"""Ordered chain of backends with optional pre/post hooks."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
language: str,
|
|
20
|
+
backends: list[str],
|
|
21
|
+
stop: str = "first",
|
|
22
|
+
pre_hook: PreHook | None = None,
|
|
23
|
+
post_hook: PostHook | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
if stop not in ("first", "all"):
|
|
26
|
+
raise ValueError(f"stop must be 'first' or 'all', got {stop!r}")
|
|
27
|
+
self.language = language
|
|
28
|
+
self._backends = backends
|
|
29
|
+
self.stop = stop
|
|
30
|
+
self._pre_hook = pre_hook
|
|
31
|
+
self._post_hook = post_hook
|
|
32
|
+
|
|
33
|
+
def run(
|
|
34
|
+
self,
|
|
35
|
+
lemma: str,
|
|
36
|
+
features: "dict[str, str] | str",
|
|
37
|
+
pos: str,
|
|
38
|
+
) -> InflectResult:
|
|
39
|
+
"""Execute the chain and return InflectResult."""
|
|
40
|
+
tried: list[str] = []
|
|
41
|
+
forms: set[str] = set()
|
|
42
|
+
source: str | None = None
|
|
43
|
+
by_backend: dict[str, set[str]] = {}
|
|
44
|
+
|
|
45
|
+
if self._pre_hook is not None:
|
|
46
|
+
ctx = HookContext(
|
|
47
|
+
lemma=lemma, features=features, pos=pos,
|
|
48
|
+
language=self.language, tried=[], stop=self.stop,
|
|
49
|
+
)
|
|
50
|
+
lemma, features, pos = self._pre_hook(lemma, features, pos, ctx)
|
|
51
|
+
|
|
52
|
+
for name in self._backends:
|
|
53
|
+
key = f"{self.language}:{name}"
|
|
54
|
+
try:
|
|
55
|
+
backend = get_backend(self.language, name)
|
|
56
|
+
except (UnsupportedLanguageError, ModuleNotFoundError, BackendLoadError):
|
|
57
|
+
log.info("Chain for %s: skipping %s (not installed)", self.language, name)
|
|
58
|
+
tried.append(f"{key} (unavailable)")
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
result = backend.inflect(lemma, features, pos, language=self.language)
|
|
63
|
+
except Exception:
|
|
64
|
+
log.debug("Chain for %s: backend %s raised, skipping", self.language, name, exc_info=True)
|
|
65
|
+
tried.append(f"{key} (error)")
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
tried.append(key)
|
|
69
|
+
by_backend[key] = set(result)
|
|
70
|
+
|
|
71
|
+
if self.stop == "first":
|
|
72
|
+
if result:
|
|
73
|
+
forms = set(result)
|
|
74
|
+
source = key
|
|
75
|
+
break
|
|
76
|
+
else:
|
|
77
|
+
forms |= result
|
|
78
|
+
|
|
79
|
+
if self._post_hook is not None:
|
|
80
|
+
ctx = HookContext(
|
|
81
|
+
lemma=lemma, features=features, pos=pos,
|
|
82
|
+
language=self.language, tried=list(tried), stop=self.stop,
|
|
83
|
+
)
|
|
84
|
+
forms = self._post_hook(forms, ctx)
|
|
85
|
+
|
|
86
|
+
return InflectResult(forms=forms, source=source, tried=tried, by_backend=by_backend)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
class UnsupportedLanguageError(Exception):
|
|
2
|
+
def __init__(self, language_code: str, backend: str | None = None) -> None:
|
|
3
|
+
self.language_code = language_code
|
|
4
|
+
if backend is not None:
|
|
5
|
+
msg = (
|
|
6
|
+
f"No backend named '{backend}' found for language '{language_code}'. "
|
|
7
|
+
f"Install a backend package that registers '{backend}' in the "
|
|
8
|
+
f"eee_project.named_backends.v1 entry point group, or register one explicitly "
|
|
9
|
+
f"with eee.register_backend({language_code!r}, instance, backend={backend!r})."
|
|
10
|
+
)
|
|
11
|
+
else:
|
|
12
|
+
msg = (
|
|
13
|
+
f"No backend registered for language '{language_code}'. "
|
|
14
|
+
f"Install a backend package (e.g., pip install {language_code}-eee) "
|
|
15
|
+
f"or register one with eee.register_backend()."
|
|
16
|
+
)
|
|
17
|
+
super().__init__(msg)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BackendLoadError(Exception):
|
|
21
|
+
def __init__(self, language_code: str, cause: Exception) -> None:
|
|
22
|
+
self.language_code = language_code
|
|
23
|
+
super().__init__(
|
|
24
|
+
f"Failed to load backend for language '{language_code}': {cause}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AmbiguousPOSError(Exception):
|
|
29
|
+
def __init__(self, lemma: str) -> None:
|
|
30
|
+
self.lemma = lemma
|
|
31
|
+
super().__init__(
|
|
32
|
+
f"POS is required but was not provided for lemma '{lemma}'."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PosNotSupportedError(Exception):
|
|
37
|
+
def __init__(self, pos: str) -> None:
|
|
38
|
+
self.pos = pos
|
|
39
|
+
super().__init__(f"POS '{pos}' is not supported by the UniMorph translator.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FeatureNotSupportedError(Exception):
|
|
43
|
+
def __init__(self, key: str, value: str) -> None:
|
|
44
|
+
self.key = key
|
|
45
|
+
self.value = value
|
|
46
|
+
super().__init__(f"UD feature '{key}={value}' has no UniMorph mapping.")
|