Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
[build-system]
requires = [
# pin setuptools on pypy to workaround this bug: https://github.com/pypa/distutils/issues/283
"setuptools >= 36.4, < 72.2; platform_python_implementation == 'PyPy'",
"setuptools >= 36.4; platform_python_implementation != 'PyPy'",
"setuptools >= 36.4",
"wheel",
"setuptools_scm >= 2.1",
"cython >= 0.28.1",
"cython >= 3.1",
"pkgconfig"
]
build-backend = "setuptools.build_meta"

[tool.setuptools_scm]

[tool.cibuildwheel]
# Skip building PyPy 3.8 wheels, the build currently fails
# Also skip wheels for free-threaded CPython 3.14 (e.g. 'cp314t') until we
# actually support them. We currently aren't thread-safe as we define HB_NO_MT=1.
skip = ["pp38-*", "cp3??t-*"]
# CPython abi3 wheels only need universal2 on macOS, skip the single-arch builds.
skip = ["cp38-*", "cp39-*", "cp3??t-*", "cp*-macosx_x86_64", "cp*-macosx_arm64"]
enable = ["pypy"]
test-requires = "pytest"
test-command = "pytest {project}/tests"
Expand All @@ -27,6 +25,21 @@ archs = ["x86_64", "universal2", "arm64"]
[tool.cibuildwheel.linux]
archs = ["native"]

# Run abi3audit after the default repair commands to scan for abi3 violations
# https://github.com/pypa/abi3audit
# Only on Unix platforms (Linux/macOS) since Windows has no default repair command
[[tool.cibuildwheel.overrides]]
select = "cp*-{linux,macosx}*"
inherit.repair-wheel-command = "append"
repair-wheel-command = "pipx run abi3audit --strict --report {wheel}"

# Disable Limited API for PyPy as the build currently fails, see:
# https://github.com/harfbuzz/uharfbuzz/issues/262#issuecomment-3415144557
# https://cibuildwheel.pypa.io/en/stable/configuration/#overrides
[[tool.cibuildwheel.overrides]]
select = "pp*"
environment = { USE_PY_LIMITED_API = "0" }


[tool.black]
extend-exclude = "harfbuzz"
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cython>=0.29.1
cython>=3.1

# we need wheel >= 0.31.0 to support Markdown long_description
wheel>=0.31
Expand Down
3 changes: 0 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
[sdist]
formats = zip

[metadata]
license_file = LICENSE
23 changes: 20 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
from setuptools import Extension, setup


def bool_from_environ(key: str):
def bool_from_environ(key: str, default: bool = False):
value = os.environ.get(key)
if not value:
return False
return default
if value == "1":
return True
if value == "0":
Expand All @@ -32,6 +32,12 @@ def bool_from_environ(key: str):
use_system_libraries = bool_from_environ("USE_SYSTEM_LIBS")
use_cython_linetrace = bool_from_environ("CYTHON_LINETRACE")
use_cython_annotate = bool_from_environ("CYTHON_ANNOTATE")
# Python Limited API for stable ABI support is enabled by default.
# Set USE_PY_LIMITED_API=0 to turn it off.
# https://docs.python.org/3.14/c-api/stable.html#limited-c-api
use_py_limited_api = bool_from_environ("USE_PY_LIMITED_API", default=True)
# NOTE: this must be kept in sync with python_requires='>=3.10' below
limited_api_min_version = "0x030A0000"


def _configure_extensions_with_system_libs() -> List[Extension]:
Expand All @@ -51,6 +57,9 @@ def _configure_extensions_with_system_libs() -> List[Extension]:
if use_cython_linetrace:
define_macros.append(("CYTHON_TRACE_NOGIL", "1"))

if use_py_limited_api:
define_macros.append(("Py_LIMITED_API", limited_api_min_version))

extension = Extension(
"uharfbuzz._harfbuzz",
define_macros=define_macros,
Expand All @@ -61,6 +70,7 @@ def _configure_extensions_with_system_libs() -> List[Extension]:
language="c++",
libraries=libraries,
library_dirs=library_dirs,
py_limited_api=use_py_limited_api,
)

extension_test = Extension(
Expand All @@ -74,6 +84,7 @@ def _configure_extensions_with_system_libs() -> List[Extension]:
language="c++",
libraries=libraries,
library_dirs=library_dirs,
py_limited_api=use_py_limited_api,
)

return [extension, extension_test]
Expand All @@ -87,6 +98,9 @@ def _configure_extensions_with_vendored_libs() -> List[Extension]:
if use_cython_linetrace:
define_macros.append(("CYTHON_TRACE_NOGIL", "1"))

if use_py_limited_api:
define_macros.append(("Py_LIMITED_API", limited_api_min_version))

extra_compile_args = []
extra_link_args = []
libraries = []
Expand Down Expand Up @@ -124,6 +138,7 @@ def _configure_extensions_with_vendored_libs() -> List[Extension]:
libraries=libraries,
extra_compile_args=extra_compile_args,
extra_link_args=extra_link_args,
py_limited_api=use_py_limited_api,
)

extension_test = Extension(
Expand All @@ -138,6 +153,7 @@ def _configure_extensions_with_vendored_libs() -> List[Extension]:
libraries=libraries,
extra_compile_args=extra_compile_args,
extra_link_args=extra_link_args,
py_limited_api=use_py_limited_api,
)

return [extension, extension_test]
Expand All @@ -164,10 +180,11 @@ def configure_extensions() -> List[Extension]:
packages=["uharfbuzz"],
zip_safe=False,
setup_requires=["setuptools_scm"],
python_requires=">=3.8",
python_requires=">=3.10",
ext_modules=cythonize(
configure_extensions(),
annotate=use_cython_annotate,
compiler_directives={"linetrace": use_cython_linetrace},
),
options={"bdist_wheel": {"py_limited_api": "cp310"}} if use_py_limited_api else {},
)
70 changes: 33 additions & 37 deletions src/uharfbuzz/_harfbuzz.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ from .charfbuzz cimport *
from libc.stdlib cimport free, malloc, calloc
from libc.string cimport const_char
from cpython.pycapsule cimport PyCapsule_GetPointer, PyCapsule_IsValid
from cpython.unicode cimport (
PyUnicode_1BYTE_DATA, PyUnicode_2BYTE_DATA, PyUnicode_4BYTE_DATA,
PyUnicode_1BYTE_KIND, PyUnicode_2BYTE_KIND, PyUnicode_4BYTE_KIND,
PyUnicode_KIND, PyUnicode_GET_LENGTH, PyUnicode_FromKindAndData
)
from cpython.unicode cimport PyUnicode_GetLength, PyUnicode_AsUCS4Copy
from cpython.mem cimport PyMem_Free
from typing import Callable, Dict, List, Sequence, Tuple, Union, NamedTuple
from pathlib import Path
from functools import wraps

# Declare Limited API types and functions (Python 3.3+)
cdef extern from "Python.h":
ctypedef uint32_t Py_UCS4


DEF STATIC_ARRAY_SIZE = 128

Expand Down Expand Up @@ -341,38 +342,27 @@ cdef class Buffer:

def add_str(self, text: str,
item_offset: int = 0, item_length: int = -1) -> None:
cdef Py_UCS4* ucs4_buffer
cdef Py_ssize_t text_length

cdef Py_ssize_t length = PyUnicode_GET_LENGTH(text)
cdef int kind = PyUnicode_KIND(text)

if kind == PyUnicode_1BYTE_KIND:
hb_buffer_add_latin1(
self._hb_buffer,
<uint8_t*>PyUnicode_1BYTE_DATA(text),
length,
item_offset,
item_length,
)
elif kind == PyUnicode_2BYTE_KIND:
hb_buffer_add_utf16(
self._hb_buffer,
<uint16_t*>PyUnicode_2BYTE_DATA(text),
length,
item_offset,
item_length,
)
elif kind == PyUnicode_4BYTE_KIND:
ucs4_buffer = PyUnicode_AsUCS4Copy(text)
if ucs4_buffer == NULL:
raise MemoryError()
try:
text_length = PyUnicode_GetLength(text)
if text_length == -1:
raise ValueError("Invalid Unicode string")
hb_buffer_add_utf32(
self._hb_buffer,
<uint32_t*>PyUnicode_4BYTE_DATA(text),
length,
<uint32_t*>ucs4_buffer,
text_length,
item_offset,
item_length,
item_length
)
else:
raise AssertionError(kind)
if not hb_buffer_allocation_successful(self._hb_buffer):
raise MemoryError()
if not hb_buffer_allocation_successful(self._hb_buffer):
raise MemoryError()
finally:
PyMem_Free(ucs4_buffer)

def guess_segment_properties(self) -> None:
hb_buffer_guess_segment_properties(self._hb_buffer)
Expand Down Expand Up @@ -952,7 +942,7 @@ cdef class Face:
def get_name(self, name_id: OTNameIdPredefined | int, language: str | None = None) -> str | None:
cdef bytes packed
cdef hb_language_t lang
cdef uint32_t *text
cdef char *text
cdef unsigned int length

if language is None:
Expand All @@ -961,12 +951,18 @@ cdef class Face:
packed = language.encode()
lang = hb_language_from_string(<char*>packed, -1)

length = hb_ot_name_get_utf32(self._hb_face, name_id, lang, NULL, NULL)
length = hb_ot_name_get_utf8(self._hb_face, name_id, lang, NULL, NULL)
if length:
length += 1 # for the null terminator
text = <uint32_t*>malloc(length * sizeof(uint32_t))
hb_ot_name_get_utf32(self._hb_face, name_id, lang, &length, text)
return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, text, length)
text = <char*>malloc(length * sizeof(char))
if text == NULL:
raise MemoryError()
try:
hb_ot_name_get_utf8(self._hb_face, name_id, lang, &length, text)
result = text[:length].decode("utf-8")
return result
finally:
free(text)
return None


Expand Down