From fbeef8cf5839f3f600496af0b7643707a012d187 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 2 Feb 2026 13:31:01 -0500 Subject: [PATCH 1/4] Python 3.15 integration --- .github/workflows/codspeed.yml | 2 +- wcwidth/grapheme.py | 40 ++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 6cc76ae..0e2ad67 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/setup-python@v6 with: - python-version: '3.14' + python-version: '3.15' cache: pip - name: Install dependencies diff --git a/wcwidth/grapheme.py b/wcwidth/grapheme.py index 7befc92..867d6d6 100644 --- a/wcwidth/grapheme.py +++ b/wcwidth/grapheme.py @@ -10,11 +10,16 @@ from __future__ import annotations # std imports +import sys +import unicodedata from enum import IntEnum from functools import lru_cache from typing import TYPE_CHECKING, NamedTuple +# check for python 3.15 for new iter_graphemes() function +_HAS_PYTHON315_ITER_GRAPHEMES = (sys.version_info >= (3, 15) and hasattr(unicodedata, 'iter_graphemes')) + # local from .bisearch import bisearch as _bisearch from .table_grapheme import (GRAPHEME_L, @@ -245,13 +250,13 @@ def _should_break( return BreakResult(should_break=True, ri_count=ri_count) -def iter_graphemes( +def _iter_graphemes_stdlib( unistr: str, start: int = 0, end: int | None = None, ) -> Iterator[str]: r""" - Iterate over grapheme clusters in a Unicode string. + Iterate over grapheme clusters using :func:`unicodedata.iter_graphemes`. Grapheme clusters are "user-perceived characters" - what a user would consider a single character, which may consist of multiple Unicode @@ -286,6 +291,30 @@ def iter_graphemes( end = min(end, length) + full_segment = unistr[start:end] + for seg in unicodedata.iter_graphemes(full_segment): + yield full_segment[seg.start:seg.end] + + +def _iter_graphemes_python( + unistr: str, + start: int = 0, + end: int | None = None, +) -> Iterator[str]: + """Pure-Python grapheme cluster iteration following UAX #29.""" + if not unistr: + return + + length = len(unistr) + + if end is None: + end = length + + if start >= end or start >= length: + return + + end = min(end, length) + # Track state for grapheme cluster boundaries cluster_start = start ri_count = 0 @@ -426,3 +455,10 @@ def iter_graphemes_reverse( break yield unistr[cluster_start:pos] pos = cluster_start + + +# Bind iter_graphemes at module level to avoid per-call dispatch overhead. +iter_graphemes = ( + _iter_graphemes_stdlib if _HAS_PYTHON315_ITER_GRAPHEMES + else _iter_graphemes_python +) From 1f6140ada28ccf4508c5cb4f33d8ce2149257f7b Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 2 Feb 2026 13:49:37 -0500 Subject: [PATCH 2/4] linting --- wcwidth/grapheme.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/wcwidth/grapheme.py b/wcwidth/grapheme.py index 867d6d6..8591c2e 100644 --- a/wcwidth/grapheme.py +++ b/wcwidth/grapheme.py @@ -17,9 +17,6 @@ from typing import TYPE_CHECKING, NamedTuple -# check for python 3.15 for new iter_graphemes() function -_HAS_PYTHON315_ITER_GRAPHEMES = (sys.version_info >= (3, 15) and hasattr(unicodedata, 'iter_graphemes')) - # local from .bisearch import bisearch as _bisearch from .table_grapheme import (GRAPHEME_L, @@ -41,6 +38,12 @@ # std imports from collections.abc import Iterator +# check for python 3.15 for new iter_graphemes() function +_HAS_PYTHON315_ITER_GRAPHEMES = ( + sys.version_info >= (3, 15) + and hasattr(unicodedata, 'iter_graphemes') +) + # Maximum backward scan distance when finding grapheme cluster boundaries. # Covers all known Unicode grapheme clusters with margin; longer sequences are pathological. MAX_GRAPHEME_SCAN = 32 @@ -292,7 +295,7 @@ def _iter_graphemes_stdlib( end = min(end, length) full_segment = unistr[start:end] - for seg in unicodedata.iter_graphemes(full_segment): + for seg in unicodedata.iter_graphemes(full_segment): # type: ignore[attr-defined] # pylint: disable=no-member yield full_segment[seg.start:seg.end] From bb837bab8280e4ae87838e522682e60c891ce45e Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 2 Feb 2026 13:52:18 -0500 Subject: [PATCH 3/4] testing/coverage and performance of python 3.15 --- .github/workflows/ci.yml | 1 + .github/workflows/codspeed.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b51e64f..4a7067c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,7 @@ jobs: - "3.12" - "3.13" - "3.14" + - "3.15" - "pypy-3.8" - "pypy-3.9" - "pypy-3.10" diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 0e2ad67..6b120f5 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -21,6 +21,7 @@ jobs: - uses: actions/setup-python@v6 with: python-version: '3.15' + allow-prereleases: true cache: pip - name: Install dependencies From 6dd215d5c229c64f431b489a29329d601fe9a6fd Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 2 Feb 2026 13:53:39 -0500 Subject: [PATCH 4/4] +pyproject.toml py315 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 795a5fa..d4c20e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Topic :: Software Development :: Internationalization", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Localization",