quran-processing-toolkit 0.4.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.
- quran_processing_toolkit/__init__.py +96 -0
- quran_processing_toolkit/__main__.py +3 -0
- quran_processing_toolkit/build.py +263 -0
- quran_processing_toolkit/chapter_names.py +142 -0
- quran_processing_toolkit/cli.py +95 -0
- quran_processing_toolkit/collections.py +615 -0
- quran_processing_toolkit/corpus.py +611 -0
- quran_processing_toolkit/models.py +672 -0
- quran_processing_toolkit/py.typed +1 -0
- quran_processing_toolkit/similarity.py +98 -0
- quran_processing_toolkit/source_data.py +201 -0
- quran_processing_toolkit/text.py +259 -0
- quran_processing_toolkit/translations.py +313 -0
- quran_processing_toolkit/validation.py +73 -0
- quran_processing_toolkit-0.4.0.dist-info/METADATA +269 -0
- quran_processing_toolkit-0.4.0.dist-info/RECORD +21 -0
- quran_processing_toolkit-0.4.0.dist-info/WHEEL +5 -0
- quran_processing_toolkit-0.4.0.dist-info/entry_points.txt +3 -0
- quran_processing_toolkit-0.4.0.dist-info/licenses/LICENSE +201 -0
- quran_processing_toolkit-0.4.0.dist-info/licenses/licenses/TANZIL-METADATA-NOTICE.txt +15 -0
- quran_processing_toolkit-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"""Typed objects representing addresses, tokens, words, verses, chapters, and juzs.
|
|
2
|
+
|
|
3
|
+
The hierarchy follows the corpus rather than Python list positions: chapters, verses,
|
|
4
|
+
words, and token parts are all addressed from one. A ``Word`` may contain several
|
|
5
|
+
``Token`` objects because QAC stores prefixes, stems, and suffixes as separate
|
|
6
|
+
morphological segments. Convenience properties on a word normally aggregate only its
|
|
7
|
+
stem tokens so that prefixes do not unexpectedly supply a root or part of speech.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .chapter_names import chapter_name
|
|
16
|
+
from .collections import OneBasedCollection, QuerySet
|
|
17
|
+
from .text import TextMixin, split_surface_words
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True, order=True)
|
|
21
|
+
class VerseAddress:
|
|
22
|
+
"""Immutable ``chapter:verse`` address using Quranic 1-based numbers."""
|
|
23
|
+
|
|
24
|
+
chapter: int
|
|
25
|
+
verse: int
|
|
26
|
+
|
|
27
|
+
def __iter__(self):
|
|
28
|
+
yield self.chapter
|
|
29
|
+
yield self.verse
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return f"{self.chapter}:{self.verse}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, slots=True, order=True)
|
|
36
|
+
class WordAddress:
|
|
37
|
+
"""Immutable ``chapter:verse:word`` address using 1-based numbers."""
|
|
38
|
+
|
|
39
|
+
chapter: int
|
|
40
|
+
verse: int
|
|
41
|
+
word: int
|
|
42
|
+
|
|
43
|
+
def __iter__(self):
|
|
44
|
+
yield self.chapter
|
|
45
|
+
yield self.verse
|
|
46
|
+
yield self.word
|
|
47
|
+
|
|
48
|
+
def as_tuple(self) -> tuple[int, int, int]:
|
|
49
|
+
"""Return the address in the form used by keyed collection lookups."""
|
|
50
|
+
return self.chapter, self.verse, self.word
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
return f"{self.chapter}:{self.verse}:{self.word}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True, order=True)
|
|
57
|
+
class TokenAddress:
|
|
58
|
+
"""Immutable ``chapter:verse:word:part`` address using 1-based numbers."""
|
|
59
|
+
|
|
60
|
+
chapter: int
|
|
61
|
+
verse: int
|
|
62
|
+
word: int
|
|
63
|
+
part: int
|
|
64
|
+
|
|
65
|
+
def __iter__(self):
|
|
66
|
+
yield self.chapter
|
|
67
|
+
yield self.verse
|
|
68
|
+
yield self.word
|
|
69
|
+
yield self.part
|
|
70
|
+
|
|
71
|
+
def as_tuple(self) -> tuple[int, int, int, int]:
|
|
72
|
+
"""Return the address in the form used by keyed collection lookups."""
|
|
73
|
+
return self.chapter, self.verse, self.word, self.part
|
|
74
|
+
|
|
75
|
+
def __str__(self) -> str:
|
|
76
|
+
return f"{self.chapter}:{self.verse}:{self.word}:{self.part}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Token(TextMixin):
|
|
80
|
+
"""One QAC morphological segment with source and convenience annotations.
|
|
81
|
+
|
|
82
|
+
Unknown attributes are looked up against the retained uppercase record keys for
|
|
83
|
+
backwards compatibility. New code should prefer the documented lowercase
|
|
84
|
+
properties such as :attr:`root`, :attr:`aspect`, and :attr:`case`.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
__slots__ = ("_data", "_nun_of_emphasis")
|
|
88
|
+
|
|
89
|
+
def __init__(self, data: dict[str, Any]) -> None:
|
|
90
|
+
self._data = data
|
|
91
|
+
# Filled when Word assembles the segments; never added to source records.
|
|
92
|
+
self._nun_of_emphasis = False
|
|
93
|
+
|
|
94
|
+
def has_nun_of_emphasis(self) -> bool:
|
|
95
|
+
"""Return whether this verb stem has an explicit ``+n:EMPH`` suffix.
|
|
96
|
+
|
|
97
|
+
The containing Word establishes suffix ownership during construction.
|
|
98
|
+
Other tokens (including the suffix itself) return False, as do standalone
|
|
99
|
+
tokens not yet assembled into a Word. This is not a spelling heuristic.
|
|
100
|
+
"""
|
|
101
|
+
return self._nun_of_emphasis
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def address(self) -> TokenAddress:
|
|
105
|
+
"""Return this segment's complete semantic address."""
|
|
106
|
+
return TokenAddress(
|
|
107
|
+
self._data["CHAPTER"],
|
|
108
|
+
self._data["VERSE"],
|
|
109
|
+
self._data["WORD"],
|
|
110
|
+
self._data["WORD_PART"],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def address_tuple(self) -> tuple[int, int, int, int]:
|
|
115
|
+
"""Return ``(chapter, verse, word, part)`` without allocating an address."""
|
|
116
|
+
return (
|
|
117
|
+
self._data["CHAPTER"],
|
|
118
|
+
self._data["VERSE"],
|
|
119
|
+
self._data["WORD"],
|
|
120
|
+
self._data["WORD_PART"],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def __getattr__(self, name: str) -> Any:
|
|
124
|
+
try:
|
|
125
|
+
return self._data[name]
|
|
126
|
+
except KeyError as error:
|
|
127
|
+
raise AttributeError(name) from error
|
|
128
|
+
|
|
129
|
+
def __repr__(self) -> str:
|
|
130
|
+
return f"Token({self.address}, {self.form!r}, tag={self.tag!r})"
|
|
131
|
+
|
|
132
|
+
def get(self, field: str, default: Any = None) -> Any:
|
|
133
|
+
"""Read an uppercase derived-record field without raising when absent."""
|
|
134
|
+
return self._data.get(field, default)
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def data(self) -> dict[str, Any]:
|
|
138
|
+
"""Return a shallow copy of all retained and derived record fields."""
|
|
139
|
+
return self._data.copy()
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def chapter(self) -> int:
|
|
143
|
+
"""Return the 1-based chapter number."""
|
|
144
|
+
return self._data["CHAPTER"]
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def verse(self) -> int:
|
|
148
|
+
"""Return the 1-based verse number within the chapter."""
|
|
149
|
+
return self._data["VERSE"]
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def word(self) -> int:
|
|
153
|
+
"""Return the 1-based word number within the verse."""
|
|
154
|
+
return self._data["WORD"]
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def part(self) -> int:
|
|
158
|
+
"""Return the 1-based morphological part number within the word."""
|
|
159
|
+
return self._data["WORD_PART"]
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def form(self) -> str:
|
|
163
|
+
"""Return the source Buckwalter surface form for this segment."""
|
|
164
|
+
return self._data["FORM"]
|
|
165
|
+
|
|
166
|
+
def get_form(self) -> str:
|
|
167
|
+
"""Return this segment's Buckwalter surface form."""
|
|
168
|
+
return self.form
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def tag(self) -> str:
|
|
172
|
+
"""Return the source QAC tag, such as ``V``, ``N``, or ``P``."""
|
|
173
|
+
return self._data["TAG"]
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def role(self) -> str:
|
|
177
|
+
"""Return ``PREFIX``, ``STEM``, or ``SUFFIX``."""
|
|
178
|
+
return self._data["TOKEN_ROLE"]
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def source_features(self) -> tuple[str, ...]:
|
|
182
|
+
"""Return the untouched non-keyed QAC features for this segment."""
|
|
183
|
+
return tuple(
|
|
184
|
+
feature for feature in self.source_feature_text.split("|") if ":" not in feature
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def source_feature_text(self) -> str:
|
|
189
|
+
"""Return the complete original QAC feature column."""
|
|
190
|
+
return self._data["FEATURES"]
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def part_of_speech(self) -> str | None:
|
|
194
|
+
"""Return the QAC ``POS`` code when the segment supplies one."""
|
|
195
|
+
return self._data.get("POS")
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def root(self) -> str | None:
|
|
199
|
+
"""Return the Buckwalter root annotation when present."""
|
|
200
|
+
return self._data.get("ROOT")
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def lemma(self) -> str | None:
|
|
204
|
+
"""Return the Buckwalter lemma annotation when present."""
|
|
205
|
+
return self._data.get("LEM")
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def transliteration(self) -> str:
|
|
209
|
+
"""Return the segment's Buckwalter surface form."""
|
|
210
|
+
return self.form
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def verb_form(self) -> str | None:
|
|
214
|
+
"""Return the derived verb-form/baab code, such as ``I`` or ``IV``."""
|
|
215
|
+
return self._data.get("VERB_FORM")
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def baab(self) -> str | None:
|
|
219
|
+
"""Alias for :attr:`verb_form`."""
|
|
220
|
+
return self.verb_form
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def aspect(self) -> str | None:
|
|
224
|
+
"""Return ``PERFECT``, ``IMPERFECT``, or ``IMPERATIVE`` when present."""
|
|
225
|
+
return self._data.get("ASPECT")
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def tense(self) -> str | None:
|
|
229
|
+
"""Return the toolkit's derived tense label when present."""
|
|
230
|
+
return self._data.get("TENSE")
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def voice(self) -> str | None:
|
|
234
|
+
"""Return ``ACTIVE`` or ``PASSIVE`` for finite verbs."""
|
|
235
|
+
return self._data.get("VOICE")
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def case(self) -> str | None:
|
|
239
|
+
"""Return the derived case/state annotation when present."""
|
|
240
|
+
return self._data.get("CASE")
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def mood(self) -> str | None:
|
|
244
|
+
"""Return the untouched keyed QAC mood annotation when present."""
|
|
245
|
+
return self._data.get("MOOD")
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def gender(self) -> str | None:
|
|
249
|
+
"""Return the derived ``MASC`` or ``FEM`` annotation when present."""
|
|
250
|
+
return self._data.get("GENDER")
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def definiteness(self) -> str | None:
|
|
254
|
+
"""Return the derived definiteness annotation when present."""
|
|
255
|
+
return self._data.get("DEFINITENESS")
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def derived_noun(self) -> str | None:
|
|
259
|
+
"""Return participle or verbal-noun classification when present."""
|
|
260
|
+
return self._data.get("DERIVED_NOUN")
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def conjugation(self) -> int | None:
|
|
264
|
+
"""Return the legacy 1-through-14 conjugation identifier when present."""
|
|
265
|
+
return self._data.get("CONJUGATE")
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def person(self) -> int | None:
|
|
269
|
+
"""Return grammatical person derived from the conjugation annotation."""
|
|
270
|
+
if self.conjugation is None:
|
|
271
|
+
return None
|
|
272
|
+
return int(_CONJUGATION_LABELS[self.conjugation][0])
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def grammatical_number(self) -> str | None:
|
|
276
|
+
"""Return ``SINGULAR``, ``DUAL``, or ``PLURAL`` when annotated."""
|
|
277
|
+
if self.conjugation is not None:
|
|
278
|
+
suffix = _CONJUGATION_LABELS[self.conjugation][-1]
|
|
279
|
+
return {"S": "SINGULAR", "D": "DUAL", "P": "PLURAL"}[suffix]
|
|
280
|
+
return self._data.get("COUNT")
|
|
281
|
+
|
|
282
|
+
def has_source_feature(self, feature: str) -> bool:
|
|
283
|
+
"""Return whether the exact non-keyed feature occurs in the source column."""
|
|
284
|
+
return feature in self.source_features
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
_CONJUGATION_LABELS = {
|
|
288
|
+
1: "3MS",
|
|
289
|
+
2: "3MD",
|
|
290
|
+
3: "3MP",
|
|
291
|
+
4: "3FS",
|
|
292
|
+
5: "3FD",
|
|
293
|
+
6: "3FP",
|
|
294
|
+
7: "2MS",
|
|
295
|
+
8: "2MD",
|
|
296
|
+
9: "2MP",
|
|
297
|
+
10: "2FS",
|
|
298
|
+
11: "2FD",
|
|
299
|
+
12: "2FP",
|
|
300
|
+
13: "1S",
|
|
301
|
+
14: "1P",
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
VERB_FORM_NAMES = {
|
|
305
|
+
"I": "Thulathy Mujarrad",
|
|
306
|
+
"II": "Taf'eel",
|
|
307
|
+
"III": "Mufaa'alah",
|
|
308
|
+
"IV": "If'aal",
|
|
309
|
+
"V": "Tafa'ul",
|
|
310
|
+
"VI": "Tafaa'ul",
|
|
311
|
+
"VII": "Infi'aal",
|
|
312
|
+
"VIII": "Ifti'aal",
|
|
313
|
+
"IX": "If'ilaal",
|
|
314
|
+
"X": "Istif'aal",
|
|
315
|
+
"XI": "If'eelaal",
|
|
316
|
+
"XII": "Other",
|
|
317
|
+
"rI": "Ruba'iy Mujarrad",
|
|
318
|
+
"rIV": "If'illaal",
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class Word(TextMixin):
|
|
323
|
+
"""One orthographic Quran word assembled from one or more QAC tokens."""
|
|
324
|
+
|
|
325
|
+
__slots__ = ("address", "tokens", "_stem_tokens", "arabic_text")
|
|
326
|
+
|
|
327
|
+
def __init__(self, address: WordAddress, tokens: list[Token], arabic_text: str) -> None:
|
|
328
|
+
self.address = address
|
|
329
|
+
self.tokens = OneBasedCollection(tokens)
|
|
330
|
+
self._stem_tokens = tuple(token for token in tokens if token.role == "STEM")
|
|
331
|
+
self.arabic_text = arabic_text
|
|
332
|
+
# A suffix belongs to the preceding stem, stopping at each new stem.
|
|
333
|
+
# EMPH also labels lam prefixes, so its tag alone is not sufficient.
|
|
334
|
+
stem: Token | None = None
|
|
335
|
+
for token in tokens:
|
|
336
|
+
if token.role == "STEM":
|
|
337
|
+
stem = token
|
|
338
|
+
stem._nun_of_emphasis = False
|
|
339
|
+
elif (
|
|
340
|
+
token.role == "SUFFIX"
|
|
341
|
+
and token.get("+n") == "EMPH"
|
|
342
|
+
and stem is not None
|
|
343
|
+
and stem.tag == "V"
|
|
344
|
+
):
|
|
345
|
+
stem._nun_of_emphasis = True
|
|
346
|
+
|
|
347
|
+
def has_nun_of_emphasis(self) -> bool:
|
|
348
|
+
"""Return whether any verb stem has an annotated nun-of-emphasis suffix."""
|
|
349
|
+
return any(token.has_nun_of_emphasis() for token in self._stem_tokens)
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def stem_tokens(self) -> QuerySet[Token]:
|
|
353
|
+
"""Return only morphological segments classified as stems."""
|
|
354
|
+
return QuerySet(self._stem_tokens)
|
|
355
|
+
|
|
356
|
+
def __repr__(self) -> str:
|
|
357
|
+
return f"Word({self.address}, {self.arabic_text!r})"
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def chapter(self) -> int:
|
|
361
|
+
"""Return the 1-based chapter number."""
|
|
362
|
+
return self.address.chapter
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def verse(self) -> int:
|
|
366
|
+
"""Return the 1-based verse number within the chapter."""
|
|
367
|
+
return self.address.verse
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def word(self) -> int:
|
|
371
|
+
"""Return the 1-based word number within the verse."""
|
|
372
|
+
return self.address.word
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def transliteration(self) -> str:
|
|
376
|
+
"""Join all token forms into the word's complete Buckwalter spelling."""
|
|
377
|
+
return "".join(token.form for token in self.tokens)
|
|
378
|
+
|
|
379
|
+
def values(self, field: str, *, stems_only: bool = True) -> tuple[Any, ...]:
|
|
380
|
+
"""Return distinct values of an uppercase token field in source order.
|
|
381
|
+
|
|
382
|
+
Stem-only aggregation is the default because affixes can have their own part
|
|
383
|
+
of speech and other annotations. Pass ``stems_only=False`` when explicitly
|
|
384
|
+
inspecting a complete segmented word.
|
|
385
|
+
"""
|
|
386
|
+
tokens = self._stem_tokens if stems_only else self.tokens
|
|
387
|
+
result: list[Any] = []
|
|
388
|
+
for token in tokens:
|
|
389
|
+
value = token.get(field)
|
|
390
|
+
if value is not None and value not in result:
|
|
391
|
+
result.append(value)
|
|
392
|
+
return tuple(result)
|
|
393
|
+
|
|
394
|
+
def value(self, field: str, *, stems_only: bool = True) -> Any:
|
|
395
|
+
"""Return ``None``, one field value, or a tuple for a multi-valued word."""
|
|
396
|
+
values = self.values(field, stems_only=stems_only)
|
|
397
|
+
if not values:
|
|
398
|
+
return None
|
|
399
|
+
return values[0] if len(values) == 1 else values
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def roots(self) -> tuple[str, ...]:
|
|
403
|
+
"""Return every distinct stem root in source order."""
|
|
404
|
+
return self.values("ROOT")
|
|
405
|
+
|
|
406
|
+
@property
|
|
407
|
+
def root(self) -> str | tuple[str, ...] | None:
|
|
408
|
+
"""Return the sole root, multiple roots, or ``None``."""
|
|
409
|
+
return self.value("ROOT")
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def lemmas(self) -> tuple[str, ...]:
|
|
413
|
+
"""Return every distinct stem lemma in source order."""
|
|
414
|
+
return self.values("LEM")
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
def lemma(self) -> str | tuple[str, ...] | None:
|
|
418
|
+
"""Return the sole lemma, multiple lemmas, or ``None``."""
|
|
419
|
+
return self.value("LEM")
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def parts_of_speech(self) -> tuple[str, ...]:
|
|
423
|
+
"""Return every distinct stem part-of-speech code."""
|
|
424
|
+
return self.values("POS")
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def part_of_speech(self) -> str | tuple[str, ...] | None:
|
|
428
|
+
"""Return one part of speech, several, or ``None``."""
|
|
429
|
+
return self.value("POS")
|
|
430
|
+
|
|
431
|
+
@property
|
|
432
|
+
def is_multi_stem(self) -> bool:
|
|
433
|
+
"""Return whether QAC represents the word with more than one stem."""
|
|
434
|
+
return len(self._stem_tokens) > 1
|
|
435
|
+
|
|
436
|
+
def has_part_of_speech(self, value: str) -> bool:
|
|
437
|
+
"""Return whether any stem has the exact QAC part-of-speech code."""
|
|
438
|
+
return value in self.parts_of_speech
|
|
439
|
+
|
|
440
|
+
def is_verb(self) -> bool:
|
|
441
|
+
"""Return whether any stem is tagged as a verb."""
|
|
442
|
+
return self.has_part_of_speech("V")
|
|
443
|
+
|
|
444
|
+
def is_noun(self) -> bool:
|
|
445
|
+
"""Return whether any stem is tagged as a noun."""
|
|
446
|
+
return self.has_part_of_speech("N")
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def verb_form(self) -> str | tuple[str, ...] | None:
|
|
450
|
+
"""Return one or more Roman-numeral verb-form codes."""
|
|
451
|
+
return self.value("VERB_FORM")
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def baab(self) -> str | tuple[str, ...] | None:
|
|
455
|
+
"""Convenience alias for :attr:`verb_form`."""
|
|
456
|
+
return self.verb_form
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def baab_name(self) -> str | tuple[str, ...] | None:
|
|
460
|
+
"""Return familiar transliterated names for the word's verb forms."""
|
|
461
|
+
forms = self.values("VERB_FORM")
|
|
462
|
+
names = tuple(VERB_FORM_NAMES.get(form, form) for form in forms)
|
|
463
|
+
return names[0] if len(names) == 1 else names or None
|
|
464
|
+
|
|
465
|
+
def get_baab(self) -> str | tuple[str, ...] | None:
|
|
466
|
+
"""Compatibility alias for the prototype's named-baab method."""
|
|
467
|
+
return self.baab_name
|
|
468
|
+
|
|
469
|
+
def get_form(self) -> str | tuple[str, ...] | None:
|
|
470
|
+
"""Return the verb form/baab code, such as ``I`` or ``IV``."""
|
|
471
|
+
return self.verb_form
|
|
472
|
+
|
|
473
|
+
def get_root(self) -> str | tuple[str, ...] | None:
|
|
474
|
+
"""Compatibility method returning :attr:`root`."""
|
|
475
|
+
return self.root
|
|
476
|
+
|
|
477
|
+
def get_lemma(self) -> str | tuple[str, ...] | None:
|
|
478
|
+
"""Compatibility method returning :attr:`lemma`."""
|
|
479
|
+
return self.lemma
|
|
480
|
+
|
|
481
|
+
@property
|
|
482
|
+
def tense(self) -> str | tuple[str, ...] | None:
|
|
483
|
+
"""Return derived convenience tense values from all stems."""
|
|
484
|
+
return self.value("TENSE")
|
|
485
|
+
|
|
486
|
+
def get_tense(self) -> str | tuple[str, ...] | None:
|
|
487
|
+
"""Compatibility method returning :attr:`tense`."""
|
|
488
|
+
return self.tense
|
|
489
|
+
|
|
490
|
+
@property
|
|
491
|
+
def aspect(self) -> str | tuple[str, ...] | None:
|
|
492
|
+
"""Return grammatical aspect values from all stems."""
|
|
493
|
+
return self.value("ASPECT")
|
|
494
|
+
|
|
495
|
+
def get_aspect(self) -> str | tuple[str, ...] | None:
|
|
496
|
+
"""Compatibility method returning :attr:`aspect`."""
|
|
497
|
+
return self.aspect
|
|
498
|
+
|
|
499
|
+
@property
|
|
500
|
+
def voice(self) -> str | tuple[str, ...] | None:
|
|
501
|
+
"""Return finite verb voice values from all stems."""
|
|
502
|
+
return self.value("VOICE")
|
|
503
|
+
|
|
504
|
+
def get_voice(self) -> str | tuple[str, ...] | None:
|
|
505
|
+
"""Compatibility method returning :attr:`voice`."""
|
|
506
|
+
return self.voice
|
|
507
|
+
|
|
508
|
+
@property
|
|
509
|
+
def conjugation(self) -> int | tuple[int, ...] | None:
|
|
510
|
+
"""Return legacy numeric conjugation identifiers from all stems."""
|
|
511
|
+
return self.value("CONJUGATE")
|
|
512
|
+
|
|
513
|
+
@property
|
|
514
|
+
def conjugation_labels(self) -> tuple[str, ...]:
|
|
515
|
+
"""Return compact person/gender/number labels such as ``3MS``."""
|
|
516
|
+
return tuple(
|
|
517
|
+
_CONJUGATION_LABELS[number]
|
|
518
|
+
for number in self.values("CONJUGATE")
|
|
519
|
+
if number in _CONJUGATION_LABELS
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
@property
|
|
523
|
+
def person(self) -> int | tuple[int, ...] | None:
|
|
524
|
+
"""Return one or more grammatical persons derived from conjugation."""
|
|
525
|
+
values = tuple(dict.fromkeys(int(label[0]) for label in self.conjugation_labels))
|
|
526
|
+
return values[0] if len(values) == 1 else values or None
|
|
527
|
+
|
|
528
|
+
def get_person(self) -> int | tuple[int, ...] | None:
|
|
529
|
+
"""Compatibility method returning :attr:`person`."""
|
|
530
|
+
return self.person
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def grammatical_number(self) -> str | tuple[str, ...] | None:
|
|
534
|
+
"""Return singular, dual, or plural from conjugation or nominal count."""
|
|
535
|
+
mapping = {"S": "SINGULAR", "D": "DUAL", "P": "PLURAL"}
|
|
536
|
+
values = tuple(dict.fromkeys(mapping[label[-1]] for label in self.conjugation_labels))
|
|
537
|
+
if values:
|
|
538
|
+
return values[0] if len(values) == 1 else values
|
|
539
|
+
# Nominals carry COUNT directly rather than through a finite-verb
|
|
540
|
+
# conjugation label. Falling back here makes with_number() useful for both.
|
|
541
|
+
return self.value("COUNT")
|
|
542
|
+
|
|
543
|
+
def get_number(self) -> str | tuple[str, ...] | None:
|
|
544
|
+
"""Compatibility method returning :attr:`grammatical_number`."""
|
|
545
|
+
return self.grammatical_number
|
|
546
|
+
|
|
547
|
+
@property
|
|
548
|
+
def gender(self) -> str | tuple[str, ...] | None:
|
|
549
|
+
"""Return masculine/feminine values from all stems."""
|
|
550
|
+
return self.value("GENDER")
|
|
551
|
+
|
|
552
|
+
def get_gender(self) -> str | tuple[str, ...] | None:
|
|
553
|
+
"""Compatibility method returning :attr:`gender`."""
|
|
554
|
+
return self.gender
|
|
555
|
+
|
|
556
|
+
@property
|
|
557
|
+
def case(self) -> str | tuple[str, ...] | None:
|
|
558
|
+
"""Return the word's derived case/state annotation."""
|
|
559
|
+
return self.value("CASE")
|
|
560
|
+
|
|
561
|
+
@property
|
|
562
|
+
def mood(self) -> str | tuple[str, ...] | None:
|
|
563
|
+
"""Return the word's untouched keyed QAC mood annotation."""
|
|
564
|
+
return self.value("MOOD")
|
|
565
|
+
|
|
566
|
+
@property
|
|
567
|
+
def definiteness(self) -> str | tuple[str, ...] | None:
|
|
568
|
+
"""Return the word's derived definiteness annotation."""
|
|
569
|
+
return self.value("DEFINITENESS")
|
|
570
|
+
|
|
571
|
+
@property
|
|
572
|
+
def derived_noun(self) -> str | tuple[str, ...] | None:
|
|
573
|
+
"""Return verbal-noun or participle classification when present."""
|
|
574
|
+
return self.value("DERIVED_NOUN")
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class Verse(TextMixin):
|
|
578
|
+
"""A Quran verse with words, Uthmani text, and an optional translation."""
|
|
579
|
+
|
|
580
|
+
__slots__ = ("address", "words", "arabic_text", "translation_text")
|
|
581
|
+
|
|
582
|
+
def __init__(
|
|
583
|
+
self,
|
|
584
|
+
address: VerseAddress,
|
|
585
|
+
words: list[Word],
|
|
586
|
+
arabic_text: str,
|
|
587
|
+
translation_text: str | None,
|
|
588
|
+
) -> None:
|
|
589
|
+
self.address = address
|
|
590
|
+
self.words = OneBasedCollection(words)
|
|
591
|
+
self.arabic_text = arabic_text
|
|
592
|
+
self.translation_text = translation_text
|
|
593
|
+
|
|
594
|
+
def __repr__(self) -> str:
|
|
595
|
+
return f"Verse({self.address}, {len(self.words)} words)"
|
|
596
|
+
|
|
597
|
+
def surface_words(self, *, ignore_pause_marks: bool = False) -> tuple[str, ...]:
|
|
598
|
+
"""Return space-delimited Arabic segments, independently of QAC words."""
|
|
599
|
+
return split_surface_words(self.arabic_text, ignore_pause_marks=ignore_pause_marks)
|
|
600
|
+
|
|
601
|
+
def get_chapter_name(self, representation: str = "transliteration") -> str:
|
|
602
|
+
"""Return this verse's chapter name in transliteration, Arabic, or English."""
|
|
603
|
+
return chapter_name(self.chapter, representation)
|
|
604
|
+
|
|
605
|
+
@property
|
|
606
|
+
def chapter(self) -> int:
|
|
607
|
+
"""Return the 1-based chapter number."""
|
|
608
|
+
return self.address.chapter
|
|
609
|
+
|
|
610
|
+
@property
|
|
611
|
+
def verse(self) -> int:
|
|
612
|
+
"""Return the 1-based verse number within the chapter."""
|
|
613
|
+
return self.address.verse
|
|
614
|
+
|
|
615
|
+
@property
|
|
616
|
+
def transliteration(self) -> str:
|
|
617
|
+
"""Join word transliterations with spaces in Quranic order."""
|
|
618
|
+
return " ".join(word.transliteration for word in self.words)
|
|
619
|
+
|
|
620
|
+
@property
|
|
621
|
+
def has_translation(self) -> bool:
|
|
622
|
+
"""Return whether a user-supplied translation covers this verse."""
|
|
623
|
+
return self.translation_text is not None
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
class Chapter:
|
|
627
|
+
"""A 1-based chapter containing a 1-based verse collection."""
|
|
628
|
+
|
|
629
|
+
__slots__ = ("number", "verses")
|
|
630
|
+
|
|
631
|
+
def __init__(self, number: int, verses: list[Verse]) -> None:
|
|
632
|
+
self.number = number
|
|
633
|
+
self.verses = OneBasedCollection(verses)
|
|
634
|
+
|
|
635
|
+
@property
|
|
636
|
+
def chapter(self) -> int:
|
|
637
|
+
"""Alias for the chapter's numeric address."""
|
|
638
|
+
return self.number
|
|
639
|
+
|
|
640
|
+
@property
|
|
641
|
+
def name(self) -> str:
|
|
642
|
+
"""Return the conventional ASCII transliterated chapter name."""
|
|
643
|
+
return self.get_name()
|
|
644
|
+
|
|
645
|
+
@property
|
|
646
|
+
def name_arabic(self) -> str:
|
|
647
|
+
"""Return the Arabic chapter name (not verse text)."""
|
|
648
|
+
return self.get_name("arabic")
|
|
649
|
+
|
|
650
|
+
@property
|
|
651
|
+
def name_english(self) -> str:
|
|
652
|
+
"""Return an English meaning of the chapter name, not a verse translation."""
|
|
653
|
+
return self.get_name("english")
|
|
654
|
+
|
|
655
|
+
def get_name(self, representation: str = "transliteration") -> str:
|
|
656
|
+
"""Return the chapter name in transliteration, Arabic, or English."""
|
|
657
|
+
return chapter_name(self.number, representation)
|
|
658
|
+
|
|
659
|
+
def __repr__(self) -> str:
|
|
660
|
+
return f"Chapter({self.number}, {len(self.verses)} verses)"
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
@dataclass(frozen=True, slots=True)
|
|
664
|
+
class Juz:
|
|
665
|
+
"""One of thirty sequential juz divisions and its ordered verses."""
|
|
666
|
+
|
|
667
|
+
number: int
|
|
668
|
+
start: VerseAddress
|
|
669
|
+
verses: QuerySet[Verse]
|
|
670
|
+
|
|
671
|
+
def __len__(self) -> int:
|
|
672
|
+
return len(self.verses)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|