Skip to content

Commit ed35ce4

Browse files
mpcabdclaude
andauthored
Modernise packaging and clean up Python 2 residue (#100)
* Modernise packaging and clean up Python 2 residue - Replace setup.py with pyproject.toml (hatchling build backend, uv-native) - Add uv.lock and dependency-groups for dev dependencies (pytest, pytest-cov) - Drop tox.ini in favour of direct `uv run pytest` - Replace .travis.yml with GitHub Actions CI (uv, Python 3.10–3.13 matrix) - Drop Python <3.10 support (3.6–3.9 all EOL) - Remove (object) base class and super() call on ArabicReshaper - Switch _ligatures_re from @Property with hidden mutation to @cached_property - Add -> str type hint and sentinel comment to reshape() - Add ArabicReshaperConfigurationError(ValueError); replace bare Exception raises - Add docstring and ImportError to config_for_true_type_font() - Fix LETTERS -> letters parameter naming in letters.py helper functions - Add comment explaining the RIAL SIGN regex pattern in ligatures.py - Remove unused import os from __init__.py; export ArabicReshaperConfigurationError - Remove debug print() from test_003_reshaping.py Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Complete remaining plan items: coverage and type hints - Wire pytest-cov into addopts so `uv run pytest` always reports coverage - Add type hints to auto_config() and config_for_true_type_font() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Strengthen font config tests; gitignore fonts/ - Add fonts/ to .gitignore so local test fonts are never committed - Skip tests gracefully when fonts/ is absent or contains no .otf files - Replace smoke tests with correctness tests: _expected_config() independently reads the font's cmap via fonttools and computes the expected config, then asserts config_for_true_type_font() == expected — catches any mis-detection of glyph support rather than just checking the function doesn't crash - Add fonttools to dev dependency group so the tests run out of the box Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Replace Travis CI badge with GitHub Actions badge in README Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Bump to 3.0.1: remove obsolete files and update version history Remove .travis.yml, MANIFEST.in, meta.yaml, and upload-to-anaconda.sh which are all superseded by the modernised pyproject.toml/hatchling setup and GitHub Actions CI. Update README version history accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add GitHub Actions release workflow for PyPI publishing Uses OIDC trusted publishing (no API token needed). Runs the full test matrix first, then builds and publishes on success. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add Python 3.14 to CI and release matrices Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c12b08f commit ed35ce4

19 files changed

Lines changed: 606 additions & 210 deletions

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Install uv
21+
uses: astral-sh/setup-uv@v5
22+
with:
23+
enable-cache: true
24+
25+
- name: Set up Python ${{ matrix.python-version }}
26+
run: uv python install ${{ matrix.python-version }}
27+
28+
- name: Install dependencies
29+
run: uv sync --group dev
30+
31+
- name: Run tests
32+
run: uv run pytest --cov=arabic_reshaper --cov-report=term-missing

.github/workflows/release.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Release
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
fail-fast: true
12+
matrix:
13+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: astral-sh/setup-uv@v5
17+
with:
18+
enable-cache: true
19+
- run: uv python install ${{ matrix.python-version }}
20+
- run: uv sync --group dev
21+
- run: uv run pytest
22+
23+
publish:
24+
needs: test
25+
runs-on: ubuntu-latest
26+
environment: pypi
27+
permissions:
28+
id-token: write
29+
steps:
30+
- uses: actions/checkout@v4
31+
- uses: astral-sh/setup-uv@v5
32+
- run: uv build
33+
- uses: pypa/gh-action-pypi-publish@release/v1

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,6 @@ venv/
4747
# ignore
4848
.ignore/
4949
.DS_Store
50+
51+
# local test fonts — not committed
52+
fonts/

.travis.yml

Lines changed: 0 additions & 37 deletions
This file was deleted.

MANIFEST.in

Lines changed: 0 additions & 7 deletions
This file was deleted.

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## Python Arabic Reshaper
22

3-
[![Build Status](https://app.travis-ci.com/mpcabd/python-arabic-reshaper.svg?branch=master)](https://app.travis-ci.com/mpcabd/python-arabic-reshaper)
3+
[![CI](https://github.com/mpcabd/python-arabic-reshaper/actions/workflows/ci.yml/badge.svg)](https://github.com/mpcabd/python-arabic-reshaper/actions/workflows/ci.yml)
44

55
Reconstruct Arabic sentences to be used in applications that don't support
66
Arabic script.
@@ -248,6 +248,14 @@ https://github.com/mpcabd/python-arabic-reshaper/tarball/master
248248

249249
## Version History
250250

251+
### 3.0.1
252+
253+
* Modernised packaging: migrated from `setuptools` to `hatchling` via `pyproject.toml`
254+
* Dropped support for Python < 3.10
255+
* Switched CI from Travis CI to GitHub Actions
256+
* Added type hints and improved test coverage for font configuration
257+
* Removed obsolete Anaconda publishing scripts
258+
251259
### 3.0.0
252260
* Stop supporting Python 2.7
253261
* Remove dependency on `future`. See #88.

arabic_reshaper/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import os
2-
31
from .arabic_reshaper import reshape, default_reshaper, ArabicReshaper
42
from .reshaper_config import (config_for_true_type_font,
3+
ArabicReshaperConfigurationError,
54
ENABLE_NO_LIGATURES,
65
ENABLE_SENTENCES_LIGATURES,
76
ENABLE_WORDS_LIGATURES,

arabic_reshaper/arabic_reshaper.py

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import re
1111

12+
from functools import cached_property
1213
from itertools import repeat
1314

1415
from .ligatures import LIGATURES
@@ -35,7 +36,7 @@
3536
)
3637

3738

38-
class ArabicReshaper(object):
39+
class ArabicReshaper:
3940
"""
4041
A class for Arabic reshaper, it allows for fine-tune configuration over the
4142
API.
@@ -56,7 +57,7 @@ class ArabicReshaper(object):
5657
"""
5758

5859
def __init__(self, configuration=None, configuration_file=None):
59-
super(ArabicReshaper, self).__init__()
60+
super().__init__()
6061

6162
self.configuration = auto_config(configuration, configuration_file)
6263
self.language = self.configuration.get('language')
@@ -68,33 +69,25 @@ def __init__(self, configuration=None, configuration_file=None):
6869
else:
6970
self.letters = LETTERS_ARABIC
7071

71-
@property
72+
@cached_property
7273
def _ligatures_re(self):
73-
if not hasattr(self, '__ligatures_re'):
74-
patterns = []
75-
re_group_index_to_ligature_forms = {}
76-
index = 0
77-
FORMS = 1
78-
MATCH = 0
79-
for ligature_record in LIGATURES:
80-
ligature, replacement = ligature_record
81-
if not self.configuration.getboolean(ligature):
82-
continue
83-
re_group_index_to_ligature_forms[index] = replacement[FORMS]
84-
patterns.append('({})'.format(replacement[MATCH]))
85-
index += 1
86-
self._re_group_index_to_ligature_forms = (
87-
re_group_index_to_ligature_forms
88-
)
89-
self.__ligatures_re = re.compile('|'.join(patterns), re.UNICODE)
90-
return self.__ligatures_re
74+
patterns = []
75+
self._re_group_index_to_ligature_forms = {}
76+
index = 0
77+
FORMS = 1
78+
MATCH = 0
79+
for ligature, replacement in LIGATURES:
80+
if not self.configuration.getboolean(ligature):
81+
continue
82+
self._re_group_index_to_ligature_forms[index] = replacement[FORMS]
83+
patterns.append(f'({replacement[MATCH]})')
84+
index += 1
85+
return re.compile('|'.join(patterns), re.UNICODE)
9186

9287
def _get_ligature_forms_from_re_group_index(self, group_index):
93-
if not hasattr(self, '_re_group_index_to_ligature_forms'):
94-
return self._ligatures_re
9588
return self._re_group_index_to_ligature_forms[group_index]
9689

97-
def reshape(self, text):
90+
def reshape(self, text: str) -> str:
9891
if not text:
9992
return ''
10093

@@ -217,6 +210,8 @@ def reshape(self, text):
217210
if not forms[ligature_form]:
218211
continue
219212
output[a] = (forms[ligature_form], NOT_SUPPORTED)
213+
# Pad the replaced positions with empty sentinels so that
214+
# Harakat position indices remain aligned with the output list.
220215
output[a+1:b] = repeat(('', NOT_SUPPORTED), b - 1 - a)
221216

222217
result = []

arabic_reshaper/letters.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -506,22 +506,22 @@
506506
ZWJ: (ZWJ, ZWJ, ZWJ, ZWJ),
507507
}
508508

509-
def connects_with_letter_before(letter,LETTERS):
510-
if letter not in LETTERS:
509+
def connects_with_letter_before(letter, letters):
510+
if letter not in letters:
511511
return False
512-
forms = LETTERS[letter]
512+
forms = letters[letter]
513513
return forms[FINAL] or forms[MEDIAL]
514514

515515

516-
def connects_with_letter_after(letter,LETTERS):
517-
if letter not in LETTERS:
516+
def connects_with_letter_after(letter, letters):
517+
if letter not in letters:
518518
return False
519-
forms = LETTERS[letter]
519+
forms = letters[letter]
520520
return forms[INITIAL] or forms[MEDIAL]
521521

522522

523-
def connects_with_letters_before_and_after(letter,LETTERS):
524-
if letter not in LETTERS:
523+
def connects_with_letters_before_and_after(letter, letters):
524+
if letter not in letters:
525525
return False
526-
forms = LETTERS[letter]
526+
forms = letters[letter]
527527
return forms[MEDIAL]

arabic_reshaper/ligatures.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
'\u0648\u0633\u0644\u0645', ('\uFDF8', '', '', ''),
7272
)),
7373
('RIAL SIGN', (
74+
# Regex (not a literal string): matches both Farsi YEH (U+06CC) and Arabic YEH (U+064A).
7475
'\u0631[\u06CC\u064A]\u0627\u0644', ('\uFDFC', '', '', ''),
7576
)),
7677
)

0 commit comments

Comments
 (0)