scripturelookup 0.1.1__tar.gz → 0.1.2__tar.gz

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 (33) hide show
  1. {scripturelookup-0.1.1/src/scripturelookup.egg-info → scripturelookup-0.1.2}/PKG-INFO +1 -1
  2. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup/data.py +25 -5
  3. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup/lookup.py +11 -5
  4. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup/patterns.py +5 -0
  5. {scripturelookup-0.1.1 → scripturelookup-0.1.2/src/scripturelookup.egg-info}/PKG-INFO +1 -1
  6. scripturelookup-0.1.2/src/scripturelookup.egg-info/scm_version.json +8 -0
  7. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/tests/test_detect.py +48 -0
  8. scripturelookup-0.1.1/src/scripturelookup.egg-info/scm_version.json +0 -8
  9. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/.github/workflows/publish.yml +0 -0
  10. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/.gitignore +0 -0
  11. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/LICENSE +0 -0
  12. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/README.md +0 -0
  13. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/pyproject.toml +0 -0
  14. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/requirements.txt +0 -0
  15. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/setup.cfg +0 -0
  16. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/geezify-python-main/LICENSE +0 -0
  17. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/geezify-python-main/README.md +0 -0
  18. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/geezify-python-main/arabify.py +0 -0
  19. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/geezify-python-main/arabify_test.py +0 -0
  20. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/geezify-python-main/geezify.py +0 -0
  21. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/geezify-python-main/geezify_test.py +0 -0
  22. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/geezify-python-main/test_data.py +0 -0
  23. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup/__init__.py +0 -0
  24. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup/command_line.py +0 -0
  25. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup/data/metadata-languages.min.json +0 -0
  26. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup/data/metadata-scriptures.min.json +0 -0
  27. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup/numbers.py +0 -0
  28. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup.egg-info/SOURCES.txt +0 -0
  29. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup.egg-info/dependency_links.txt +0 -0
  30. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup.egg-info/entry_points.txt +0 -0
  31. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup.egg-info/requires.txt +0 -0
  32. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup.egg-info/scm_file_list.json +0 -0
  33. {scripturelookup-0.1.1 → scripturelookup-0.1.2}/src/scripturelookup.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scripturelookup
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Python and command-line utility for converting scripture references between formats.
5
5
  Author-email: Samuel Bradshaw <samuel.h.bradshaw@gmail.com>
6
6
  License: MIT License
@@ -144,11 +144,15 @@ highest_book_number = len(roman_numeral_words)
144
144
  ordinal_forms_cache = {}
145
145
  def get_ordinal_forms(lang):
146
146
  if lang not in ordinal_forms_cache:
147
- ordinal_forms_cache[lang] = {
148
- form.lower(): index + 1
149
- for index, group in enumerate(ordinal_words.get(lang, []))
150
- for form in group
151
- }
147
+ forms = {}
148
+ for index, group in enumerate(ordinal_words.get(lang, [])):
149
+ for form in group:
150
+ forms[form.lower()] = index + 1
151
+ # The ordinal indicators are hard to type on many keyboards, so "1º" is also accepted as "1o" and
152
+ # "1ª" as "1a". They're derived rather than listed, so a language only spells out what's linguistic.
153
+ plain_form = form.lower().replace('º', 'o').replace('ª', 'a')
154
+ forms.setdefault(plain_form, index + 1)
155
+ ordinal_forms_cache[lang] = forms
152
156
  return ordinal_forms_cache[lang]
153
157
 
154
158
  # Get a map of number form to the number it stands for, for the prefix on a numbered book name. It's the
@@ -163,6 +167,22 @@ def get_book_number_forms(lang):
163
167
  book_number_forms_cache[lang] = forms
164
168
  return book_number_forms_cache[lang]
165
169
 
170
+ # Get the maps above keyed for comparison, so the number a matched form stands for can be looked up however
171
+ # the form was spelled. The pattern built from these forms matches text the map has no key for: a character
172
+ # class matches "Decimo" where the map holds "décimo", and whitespace is matched loosely, so "décimo
173
+ # primero" can arrive with a non-breaking space between the words.
174
+ normalized_ordinal_forms_cache = {}
175
+ def get_normalized_ordinal_forms(lang):
176
+ if lang not in normalized_ordinal_forms_cache:
177
+ normalized_ordinal_forms_cache[lang] = {normalize_for_compare(form): number for form, number in get_ordinal_forms(lang).items()}
178
+ return normalized_ordinal_forms_cache[lang]
179
+
180
+ normalized_book_number_forms_cache = {}
181
+ def get_normalized_book_number_forms(lang):
182
+ if lang not in normalized_book_number_forms_cache:
183
+ normalized_book_number_forms_cache[lang] = {normalize_for_compare(form): number for form, number in get_book_number_forms(lang).items()}
184
+ return normalized_book_number_forms_cache[lang]
185
+
166
186
  # List conjunctions from every language, split into the ones written as a word ("and", "y", "et") and the ones written as a symbol ("&")
167
187
  all_list_conjunctions = sorted({c for words in reference_words.values() for c in words['list_conjunctions']})
168
188
  list_conjunction_words = [c for c in all_list_conjunctions if any(char.isalpha() for char in c)]
@@ -201,7 +201,8 @@ def get_numbered_book_patterns(lang):
201
201
  numbered_book_pattern_cache[lang] = {
202
202
  'fragment': r'|'.join(rf'(?:{form_pattern})\s+(?:{stem_pattern})' for form_pattern, stem_pattern in branches),
203
203
  'rewrite': re.compile(rf'{patterns.reference_start_boundary_pattern}({rewrite_alternation})\s+', flags=re.IGNORECASE),
204
- 'number_by_form': number_by_form,
204
+ # Keyed for comparison, since the patterns above match spellings the plain forms don't cover
205
+ 'number_by_form': data.get_normalized_book_number_forms(lang),
205
206
  }
206
207
  return numbered_book_pattern_cache[lang]
207
208
 
@@ -239,7 +240,8 @@ def get_ordinal_patterns(lang):
239
240
  ordinal_run = rf'(?:{ordinals})(?:(?:{joiner_alternation})(?:{ordinals}))*'
240
241
 
241
242
  ordinal_pattern_cache[lang] = {
242
- 'number_by_form': number_by_form,
243
+ # Keyed for comparison, since the patterns above match spellings the plain forms don't cover
244
+ 'number_by_form': data.get_normalized_ordinal_forms(lang),
243
245
  # Spliced into the detection pattern as an anchor that needs no number after it – the ordinal is the number
244
246
  'fragment': rf'(?:{ordinal_run})\s+(?:{books})',
245
247
  'rewrite': re.compile(rf'{patterns.reference_start_boundary_pattern}({ordinal_run})\s+({books}){patterns.not_followed_by_letter_pattern}', flags=re.IGNORECASE),
@@ -263,7 +265,7 @@ def replace_ordinal_references(input_string, lang):
263
265
 
264
266
  def replace(match):
265
267
  ordinal_run, book_name = match.group(1), match.group(2)
266
- ordinal_numbers = [ordinal_patterns['number_by_form'][ordinal.group(0).lower()] for ordinal in ordinal_patterns['single_ordinal'].finditer(ordinal_run)]
268
+ ordinal_numbers = [ordinal_patterns['number_by_form'][data.normalize_for_compare(ordinal.group(0))] for ordinal in ordinal_patterns['single_ordinal'].finditer(ordinal_run)]
267
269
  is_range = len(ordinal_numbers) > 1 and ordinal_patterns['range_conjunction'] and ordinal_patterns['range_conjunction'].search(ordinal_run)
268
270
  numbers_string = f'{ordinal_numbers[0]}-{ordinal_numbers[-1]}' if is_range else ','.join(str(number) for number in ordinal_numbers)
269
271
  return f'{book_name} {numbers_string}'
@@ -278,7 +280,7 @@ def replace_book_number_prefixes(input_string, lang):
278
280
  return input_string
279
281
 
280
282
  def replace(match):
281
- return str(numbered_book_patterns['number_by_form'][match.group(1).lower()]) + ' '
283
+ return str(numbered_book_patterns['number_by_form'][data.normalize_for_compare(match.group(1))]) + ' '
282
284
  return numbered_book_patterns['rewrite'].sub(replace, input_string)
283
285
 
284
286
 
@@ -507,6 +509,8 @@ def parse_verses_string(verses_string, lang = 'en', range_split_limit = 1):
507
509
  verses_string = (verses_string or '').replace('p', '').strip()
508
510
  if not verses_string:
509
511
  return None
512
+ # A footnote letter marks a study note, not part of the verse, so it's dropped. Example: "7a" –> "7"
513
+ verses_string = patterns.verse_footnote_letter.sub('', verses_string)
510
514
 
511
515
  unique_verses = set()
512
516
  all_verses_are_integers = True
@@ -732,7 +736,9 @@ def parse_references_string(input_string, lang = 'en', sort_by = None, skip_clea
732
736
  unparsed = input_string
733
737
  verses_string = ''
734
738
  verses_string, remaining_context_verses_string = (patterns.opening_parenthesis.split(patterns.closing_parenthesis.sub('', verses_string)) + [''])[:2]
735
- if context_verses_string is None:
739
+ # Only numbers count as context verses, so stray words after an unclosed parenthesis are dropped. Example: "Matthew 7:1–2 (see" –> "Matthew 7:1–2"
740
+ remaining_context_verses_string = remaining_context_verses_string.strip()
741
+ if context_verses_string is None and patterns.numbers_and_separators.match(remaining_context_verses_string):
736
742
  context_verses_string = remaining_context_verses_string
737
743
 
738
744
  # Get chapter string and book string
@@ -56,6 +56,11 @@ chapter_range = re.compile(rf'\d+(?:{verse_range_separators_pattern})\d+')
56
56
  number_continuation_pattern = rf'\d|\s|{chapter_verse_separators_pattern}|{verse_range_separators_pattern}|{verse_group_separators_pattern}'
57
57
  # A string made up entirely of numbers and separators. Example: "3, 5–7"
58
58
  numbers_and_separators = re.compile(rf'^\d(?:{number_continuation_pattern})*$')
59
+ # A footnote letter after a verse number. Example: "6:7a" –> "6:7"
60
+ # Only parsing strips it – the detection patterns stop at the digit, so a detected span covers "Alma 32:21"
61
+ # rather than "Alma 32:21a". Letting detection take a trailing letter would swallow the Spanish and
62
+ # Portuguese range conjunction, as in "Alma 32:21 a 23".
63
+ verse_footnote_letter = re.compile(rf'(?<=\d)[a-zA-Z]{not_followed_by_letter_pattern}')
59
64
  # The chapter and verses at the end of a reference, so the book name can be split off the front
60
65
  trailing_chapter = re.compile(rf'^.*?(\d(?:{number_continuation_pattern})*)$')
61
66
  # Text after the end of a reference. Example: "1 John 3:2 2" –> " 2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scripturelookup
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Python and command-line utility for converting scripture references between formats.
5
5
  Author-email: Samuel Bradshaw <samuel.h.bradshaw@gmail.com>
6
6
  License: MIT License
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.1.2",
3
+ "distance": 0,
4
+ "node": "gae49c65f49fb9037317e89a50a7d60cc7ce68ca9",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-20"
8
+ }
@@ -255,6 +255,31 @@ def test_spaced_out_verses_are_not_stripped_as_trailing_text():
255
255
  assert lookup.get_label('1 John 3:2 2') == '1\xa0John\xa03:2'
256
256
 
257
257
 
258
+ # Words inside a parenthetical aren't context verses, even when a second reference splits the parenthetical apart
259
+ def test_words_in_a_parenthetical_are_not_context_verses():
260
+ text = 'Matthew 7:1–2 (see JST Matthew 7:1–2)'
261
+ assert lookup.get_church_uri(text) == '/scriptures/nt/matt/7.1-2\n/scriptures/jst/jst-matt/7.1-2'
262
+ assert lookup.get_church_uri(text, use_query_parameters = True) == '/scriptures/nt/matt/7?id=p1-p2\n/scriptures/jst/jst-matt/7?id=p1-p2'
263
+ # ...while numbers still are
264
+ assert lookup.get_church_uri('Gen. 1:3 (3–4)') == '/scriptures/ot/gen/1.3(3-4)'
265
+
266
+
267
+ # A footnote letter after a verse marks a study note, so it's dropped rather than taking the verse with it
268
+ @pytest.mark.parametrize('text, expected_uri', [
269
+ ('Genesis 6:7a', '/scriptures/ot/gen/6.7'),
270
+ ('Genesis 6:7a, 13', '/scriptures/ot/gen/6.7,13'),
271
+ ('Alma 32:21a–23b', '/scriptures/bofm/alma/32.21-23'),
272
+ ('Gen. 1:3a (3–4)', '/scriptures/ot/gen/1.3(3-4)'),
273
+ ])
274
+ def test_verse_footnote_letters_are_dropped(text, expected_uri):
275
+ assert lookup.get_church_uri(text) == expected_uri
276
+
277
+
278
+ # A footnote letter is citation syntax rather than sloppy input, so it goes even when cleanup is skipped
279
+ def test_verse_footnote_letters_are_dropped_even_when_cleanup_is_skipped():
280
+ assert lookup.get_church_uri('Genesis 6:7a', skip_cleanup = True) == '/scriptures/ot/gen/6.7'
281
+
282
+
258
283
  # A book with only one chapter takes a bare number as its verse – there is no Jude 3
259
284
  @pytest.mark.parametrize('text, expected_uris', [
260
285
  ('Jude 3', ['/scriptures/nt/jude/1.3']),
@@ -291,11 +316,20 @@ def test_single_chapter_rule_applies_even_when_cleanup_is_skipped():
291
316
  ('Premier Néphi 3:7', 'fr', '/scriptures/bofm/1-ne/3.7'),
292
317
  ('1er Néphi 3:7', 'fr', '/scriptures/bofm/1-ne/3.7'),
293
318
  ('Primer Nefi 3:7', 'es', '/scriptures/bofm/1-ne/3.7'),
319
+ ('1o Nefi 3:7', 'es', '/scriptures/bofm/1-ne/3.7'),
320
+ ('3a Nefi 11', 'es', '/scriptures/bofm/3-ne/11'),
321
+ ('2o Néfi 2:25', 'pt', '/scriptures/bofm/2-ne/2.25'),
294
322
  ])
295
323
  def test_book_numbers_may_be_written_out(text, lang, expected_uri):
296
324
  assert lookup.get_church_uri(text, lang = lang) == expected_uri
297
325
 
298
326
 
327
+ # "1o" and "1a" stand in for "1º" and "1ª", but a verse with a footnote letter is never read as an ordinal
328
+ def test_verse_letters_are_not_ordinals():
329
+ assert lookup.detect_references('Ver Génesis 6:7a Nefi 3:7.', lang = 'es') == [['Génesis 6:7', 4, 15]]
330
+ assert lookup.detect_references('Ver Alma 32:21a y 3a Nefi 11.', lang = 'es') == [['Alma 32:21', 4, 14], ['3a Nefi 11', 18, 28]]
331
+
332
+
299
333
  # An article of faith cited by position is that article, since the book is a single chapter
300
334
  @pytest.mark.parametrize('text, lang, expected_uris', [
301
335
  ('First Article of Faith', 'en', ['/scriptures/pgp/a-of-f/1.1']),
@@ -304,6 +338,8 @@ def test_book_numbers_may_be_written_out(text, lang, expected_uri):
304
338
  ('1er article de foi', 'fr', ['/scriptures/pgp/a-of-f/1.1']),
305
339
  ('Primer Artículo de Fe', 'es', ['/scriptures/pgp/a-of-f/1.1']),
306
340
  ('Primeira regra de fé', 'pt', ['/scriptures/pgp/a-of-f/1.1']),
341
+ ('1o artículo de fe', 'es', ['/scriptures/pgp/a-of-f/1.1']),
342
+ ('1a regra de fé', 'pt', ['/scriptures/pgp/a-of-f/1.1']),
307
343
  ('First and Third Articles of Faith', 'en', ['/scriptures/pgp/a-of-f/1.1', '/scriptures/pgp/a-of-f/1.3']),
308
344
  ('First through Third Articles of Faith', 'en', ['/scriptures/pgp/a-of-f/1.1-3']),
309
345
  ('Premier et troisième articles de foi', 'fr', ['/scriptures/pgp/a-of-f/1.1', '/scriptures/pgp/a-of-f/1.3']),
@@ -353,6 +389,18 @@ def test_abbreviated_ordinals_as_book_numbers(text, lang, expected_uri):
353
389
  assert lookup.get_church_uri(text, lang = lang) == expected_uri
354
390
 
355
391
 
392
+ # An ordinal matched with its accent left out resolves to the same number, since the character classes in the
393
+ # pattern accept spellings that aren't listed as forms
394
+ @pytest.mark.parametrize('text, lang, expected_uri', [
395
+ ('Decimo articulo de fe', 'es', '/scriptures/pgp/a-of-f/1.10'),
396
+ ('decimo primer articulo de fe', 'es', '/scriptures/pgp/a-of-f/1.11'),
397
+ ('Deuxieme article de foi', 'fr', '/scriptures/pgp/a-of-f/1.2'),
398
+ ('Premier Nephi 3:7', 'fr', '/scriptures/bofm/1-ne/3.7'),
399
+ ])
400
+ def test_ordinals_resolve_without_their_accents(text, lang, expected_uri):
401
+ assert lookup.get_church_uri(text, lang = lang) == expected_uri
402
+
403
+
356
404
  # Rewriting an ordinal or a written-out book number is cleanup, so skip_cleanup skips it
357
405
  def test_skip_cleanup_leaves_written_out_numbers_alone():
358
406
  assert lookup.get_church_uri('Premier article de foi', lang = 'fr', skip_cleanup = True) == ''
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.1.1",
3
- "distance": 0,
4
- "node": "g23aad54fd3c9ffe72276ccacf9b1a806ac7c05cb",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-08-29"
8
- }
File without changes