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.
Files changed (42) hide show
  1. eee_project/__init__.py +460 -0
  2. eee_project/_chain.py +86 -0
  3. eee_project/_exceptions.py +46 -0
  4. eee_project/_grammar_fmt.py +103 -0
  5. eee_project/_hooks.py +28 -0
  6. eee_project/_protocol.py +49 -0
  7. eee_project/_registry.py +246 -0
  8. eee_project/_results.py +40 -0
  9. eee_project/_slot_template.py +50 -0
  10. eee_project/_tag_registry.py +55 -0
  11. eee_project/backends/__init__.py +0 -0
  12. eee_project/backends/_mg_features.py +158 -0
  13. eee_project/backends/modern_greek.py +122 -0
  14. eee_project/backends/unimorph.py +255 -0
  15. eee_project/backends/unimorph_tags.py +36 -0
  16. eee_project/data/__init__.py +0 -0
  17. eee_project/data/labels/__init__.py +0 -0
  18. eee_project/data/labels/adj-el.tsv +41 -0
  19. eee_project/data/labels/adj-en.tsv +41 -0
  20. eee_project/data/labels/adj-ru.tsv +41 -0
  21. eee_project/data/labels/noun-el.tsv +41 -0
  22. eee_project/data/labels/noun-en.tsv +41 -0
  23. eee_project/data/labels/noun-ru.tsv +41 -0
  24. eee_project/data/labels/tense-el.tsv +15 -0
  25. eee_project/data/labels/tense-en.tsv +15 -0
  26. eee_project/data/labels/tense-ru.tsv +15 -0
  27. eee_project/data/labels/ui-el.tsv +35 -0
  28. eee_project/data/labels/ui-en.tsv +35 -0
  29. eee_project/data/labels/ui-ru.tsv +35 -0
  30. eee_project/data/labels/verb-el.tsv +193 -0
  31. eee_project/data/labels/verb-en.tsv +193 -0
  32. eee_project/data/labels/verb-ru.tsv +193 -0
  33. eee_project/data/languages.yaml +45 -0
  34. eee_project/data/unimorph/ell.tsv +211923 -0
  35. eee_project/data/unimorph/grc.tsv +44026 -0
  36. eee_project/notebook_utils.py +5686 -0
  37. eee_project-1.0.0.dist-info/METADATA +161 -0
  38. eee_project-1.0.0.dist-info/RECORD +42 -0
  39. eee_project-1.0.0.dist-info/WHEEL +4 -0
  40. eee_project-1.0.0.dist-info/licenses/AUTHORS +1 -0
  41. eee_project-1.0.0.dist-info/licenses/LICENSE +73 -0
  42. eee_project-1.0.0.dist-info/licenses/NOTICE +27 -0
@@ -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.")