-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add python/cython interface for unicode-normalizer APIs #23896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
1449968
Add python/cython interface for unicode-normalizer APIs
davidwendt e1a8c5b
fix cython semantics per AI review
davidwendt 03243c4
Merge branch 'main' into python-normalize-nfkc
davidwendt d3beb50
Merge branch 'main' into python-normalize-nfkc
davidwendt ff7fe70
add from_python_unicodedata classmethod
davidwendt face831
add form parameter check and pytest
davidwendt 4e757f8
Merge branch 'main' into python-normalize-nfkc
davidwendt 9014db2
fix imports; add pytest.fixture
davidwendt 8514ede
Merge branch 'main' into python-normalize-nfkc
davidwendt 628138a
Merge branch 'main' into python-normalize-nfkc
davidwendt 8baeb3b
Merge branch 'main' into python-normalize-nfkc
davidwendt a6ede7c
Merge branch 'main' into python-normalize-nfkc
davidwendt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Unicode TR15 normalization support for cudf strings columns.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import unicodedata as ud | ||
|
|
||
| import pylibcudf as plc | ||
|
|
||
| from cudf.core.column.column import ColumnBase | ||
| from cudf.core.dataframe import DataFrame | ||
| from cudf.core.series import Series | ||
|
|
||
|
|
||
| class UnicodeNormalizer: | ||
| """ | ||
| A normalizer object for Unicode TR15 normalization (NFD, NFC, NFKD, NFKC). | ||
|
|
||
| The normalizer is constructed from the contents of the Unicode Character | ||
| Database ``UnicodeData.txt`` file, which must be loaded by the caller as a | ||
| :class:`cudf.DataFrame` (e.g. via :func:`cudf.read_csv`). | ||
|
|
||
| The file is published by the Unicode Consortium and can be downloaded from: | ||
| https://unicode.org/Public/15.1.0/ucd/UnicodeData.txt | ||
|
|
||
| It is a semicolon-delimited file with 15 fields per row. The three fields | ||
| required by this API are field 0 (code point hex), field 3 (CCC), and | ||
| field 5 (decomposition mapping). Load only those three columns: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| unicode_data = cudf.read_csv( | ||
| "UnicodeData.txt", | ||
| sep=";", | ||
| header=None, | ||
| usecols=[0, 3, 5], | ||
| dtype={0: "str", 3: "int32", 5: "str"}, | ||
| ) | ||
| normalizer = UnicodeNormalizer(unicode_data, form="NFKC") | ||
| result = normalizer.normalize(series) | ||
|
|
||
| The ``unicode_data`` DataFrame must contain exactly three columns in the | ||
| following order: | ||
|
|
||
| - column[0]: ``str`` Code point values as uppercase hex strings (e.g. ``"00C9"``) | ||
| - column[1]: ``int32`` Canonical_Combining_Class (CCC) values in range [0, 254] | ||
| - column[2]: ``str`` Decomposition_Mapping field; empty string for identity | ||
| mappings, optionally prefixed with a compatibility tag such as | ||
| ``<compat>``, ``<font>``, ``<wide>``, etc. | ||
|
|
||
| Decomposition of Hangul syllables (U+AC00..U+D7A3) is performed | ||
| algorithmically per the Unicode standard and does not require entries in | ||
| the provided table. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| unicode_data : cudf.DataFrame | ||
| Three-column DataFrame loaded from ``UnicodeData.txt`` as described above. | ||
| form : str | ||
| Normalization form: one of ``"NFD"``, ``"NFC"``, ``"NFKD"``, | ||
| or ``"NFKC"``. | ||
| """ | ||
|
|
||
| _FORM_MAP = { | ||
| "NFD": plc.nvtext.unicode_normalize.UnicodeNormalizationForm.NFD, | ||
| "NFC": plc.nvtext.unicode_normalize.UnicodeNormalizationForm.NFC, | ||
| "NFKD": plc.nvtext.unicode_normalize.UnicodeNormalizationForm.NFKD, | ||
| "NFKC": plc.nvtext.unicode_normalize.UnicodeNormalizationForm.NFKC, | ||
| } | ||
|
|
||
| def __init__( | ||
| self, | ||
| unicode_data: DataFrame, | ||
| form: str = "NFC", | ||
| ) -> None: | ||
| if form not in self._FORM_MAP: | ||
| raise ValueError( | ||
| f"Invalid normalization form {form!r}. " | ||
| f"Expected one of: {list(self._FORM_MAP)}" | ||
| ) | ||
| tbl, _ = unicode_data.to_pylibcudf() | ||
| self._normalizer = plc.nvtext.unicode_normalize.UnicodeNormalizer( | ||
| tbl, self._FORM_MAP[form] | ||
| ) | ||
|
|
||
| @classmethod | ||
| def from_python_unicodedata(cls, form: str = "NFC") -> UnicodeNormalizer: | ||
| """ | ||
| Construct a :class:`UnicodeNormalizer` from Python's built-in | ||
| ``unicodedata`` module, without requiring a ``UnicodeData.txt`` file. | ||
|
|
||
| This is equivalent to downloading ``UnicodeData.txt``, loading it with | ||
| :func:`cudf.read_csv`, and passing the result to | ||
| :class:`UnicodeNormalizer`, but derives the same three columns directly | ||
| from :mod:`unicodedata`: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import unicodedata as ud | ||
|
|
||
| cp_list, ccc_list, decomp_list = [], [], [] | ||
| for cp in range(0x0080, 0x110000): | ||
| c = chr(cp) | ||
| ccc = ud.combining(c) | ||
| decomp = ud.decomposition(c) | ||
| if ccc != 0 or decomp: | ||
| cp_list.append(f"{cp:04X}") | ||
| ccc_list.append(ccc) | ||
| decomp_list.append(decomp) | ||
|
|
||
| The Unicode version used is whichever version is bundled with the | ||
| running Python interpreter (see ``unicodedata.unidata_version``), which | ||
| may differ from a specific ``UnicodeData.txt`` download. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| form : str | ||
| Normalization form: one of ``"NFD"``, ``"NFC"``, ``"NFKD"``, | ||
| or ``"NFKC"``. | ||
|
|
||
| Returns | ||
| ------- | ||
| UnicodeNormalizer | ||
| Ready-to-use normalizer for the requested form. | ||
| """ | ||
| if form not in cls._FORM_MAP: | ||
| raise ValueError( | ||
| f"Invalid normalization form {form!r}. " | ||
| f"Expected one of: {list(cls._FORM_MAP)}" | ||
| ) | ||
|
|
||
| cp_list: list[str] = [] | ||
| ccc_list: list[int] = [] | ||
| decomp_list: list[str] = [] | ||
| for cp in range(0x0080, 0x110000): | ||
| c = chr(cp) | ||
| ccc = ud.combining(c) | ||
| decomp = ud.decomposition(c) | ||
| if ccc != 0 or decomp: | ||
| cp_list.append(f"{cp:04X}") | ||
| ccc_list.append(ccc) | ||
| decomp_list.append(decomp) | ||
|
|
||
| unicode_data = DataFrame( | ||
| { | ||
| "cp": Series(cp_list), | ||
| "ccc": Series(ccc_list, dtype="int32"), | ||
| "decomp": Series(decomp_list), | ||
| } | ||
| ) | ||
| return cls(unicode_data, form=form) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| def normalize(self, text: Series) -> Series: | ||
| """ | ||
| Normalize a strings Series using Unicode TR15 normalization. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| text : cudf.Series | ||
| The UTF-8 strings to normalize. Null entries produce null output. | ||
|
|
||
| Returns | ||
| ------- | ||
| cudf.Series | ||
| New Series of normalized UTF-8 strings. | ||
| """ | ||
| plc_column = plc.nvtext.unicode_normalize.normalize_unicode( | ||
| text._column.plc_column, self._normalizer | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| return Series._from_column( | ||
| ColumnBase.create(plc_column, text._column.dtype), | ||
| name=text.name, | ||
| index=text.index, | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
python/pylibcudf/pylibcudf/libcudf/nvtext/unicode_normalize.pxd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| from libc.stdint cimport int32_t | ||
| from libcpp.memory cimport unique_ptr | ||
| from pylibcudf.exception_handler cimport libcudf_exception_handler | ||
| from pylibcudf.libcudf.column.column cimport column | ||
| from pylibcudf.libcudf.column.column_view cimport column_view | ||
| from pylibcudf.libcudf.table.table_view cimport table_view | ||
| from cuda.bindings.cyruntime cimport cudaStream_t | ||
| from rmm.librmm.memory_resource cimport device_async_resource_ref | ||
|
|
||
|
|
||
| cdef extern from "nvtext/unicode_normalize.hpp" namespace "nvtext" nogil: | ||
|
|
||
| cpdef enum class unicode_normalization_form(int32_t): | ||
| NFD | ||
| NFC | ||
| NFKD | ||
| NFKC | ||
|
|
||
| cdef struct unicode_normalizer: | ||
| pass | ||
|
|
||
| cdef unique_ptr[unicode_normalizer] create_unicode_normalizer( | ||
| const table_view &unicode_data, | ||
| unicode_normalization_form form, | ||
| cudaStream_t stream, | ||
| device_async_resource_ref mr | ||
| ) except +libcudf_exception_handler | ||
|
|
||
| cdef unique_ptr[column] normalize_unicode( | ||
| const column_view &input, | ||
| const unicode_normalizer &normalizer, | ||
| cudaStream_t stream, | ||
| device_async_resource_ref mr | ||
| ) except +libcudf_exception_handler | ||
|
davidwendt marked this conversation as resolved.
|
||
2 changes: 2 additions & 0 deletions
2
python/pylibcudf/pylibcudf/libcudf/nvtext/unicode_normalize.pyx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. | ||
| # SPDX-License-Identifier: Apache-2.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| from libcpp.memory cimport unique_ptr | ||
| from pylibcudf.column cimport Column | ||
| from pylibcudf.libcudf.nvtext.unicode_normalize cimport ( | ||
| unicode_normalizer as cpp_unicode_normalizer, | ||
| unicode_normalization_form as cpp_unicode_normalization_form, | ||
| ) | ||
| from rmm.pylibrmm.memory_resource cimport DeviceMemoryResource | ||
|
|
||
|
|
||
| cdef class UnicodeNormalizer: | ||
| cdef unique_ptr[cpp_unicode_normalizer] c_obj | ||
|
|
||
| cpdef Column normalize_unicode( | ||
| Column input, | ||
| UnicodeNormalizer normalizer, | ||
| object stream=*, | ||
| DeviceMemoryResource mr=*, | ||
| ) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we ship this data with libcudf? Are there issues with licensing, data size, keeping the file up to date, etc.? I think asking users to provide their own data file here is annoying, especially because other normalizers I've worked with do not have a similar requirement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've no opinion. We could include one in our package. I suppose that is what the python unicode library does? I don't expect it to change in any concerning frequency. I would need help with the process here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really want to include the table inside of libcudf source if that is what you are suggesting. I would rather include the file in the python package and perhaps hide the loading into libcudf in a cudf or pylibcudf wrapper.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may be able to use the Python's
unicodedatalibrary to generate the 3 needed fields by default.(From an agent), from iterating over the available hex points,
unicodedata.combininggives you the CCC andunicodedata.decompositioncan generate the Decomposition_Mapping field.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that idea. @davidwendt Do you want to give that a shot?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You bet. I like that idea as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This turned out to be fairly straightforward. I've added a
from_python_unicodedataclassmethod (and pytest). There is no code change required for libcudf/nvtext. I've kept the existing code path as well since it could be useful in case of a custom or version-specific UnicodeData.txt file different than whatever is embedded in the unicodedata library.