diff --git a/.gitignore b/.gitignore index ad8e4f33..cf6fcf38 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ h3c/ .cache/ h3/out/ .ruff_cache/ +_build/ # Generated C code from Cython test tests/**/*.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 77fc0912..53497899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,17 @@ avoid adding features or APIs which do not map onto the ## Unreleased -None. +- Update `h3lib` to v4.4.1. (#470) + - Add new error codes: + - `H3IndexInvalidError` + - `H3BaseCellDomainError` + - `H3DigitDomainError` + - `H3DeletedDigitError` + - Add new functions: + - `is_valid_index` + - `get_index_digit` + - `construct_cell` + - `deconstruct_cell` ## [4.3.1] - 2025-08-10 diff --git a/docs/api_quick.md b/docs/api_quick.md index 7b296384..6694ed4a 100644 --- a/docs/api_quick.md +++ b/docs/api_quick.md @@ -25,6 +25,7 @@ and should be generally aligned with the is_res_class_III is_valid_directed_edge is_valid_vertex + is_valid_index ``` ## Index representation @@ -49,6 +50,9 @@ and should be generally aligned with the get_num_cells get_resolution get_base_cell_number + get_index_digit + construct_cell + deconstruct_cell ``` ## Geographic coordinates diff --git a/pyproject.toml b/pyproject.toml index 9f88ce87..4a5e3193 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = 'scikit_build_core.build' [project] name = 'h3' -version = '4.3.1' +version = '4.4.0' description = "Uber's hierarchical hexagonal geospatial indexing system" readme = 'readme.md' license = {file = 'LICENSE'} @@ -50,7 +50,7 @@ numpy = ['numpy'] test = ['pytest', 'pytest-cov', 'ruff', 'numpy'] all = [ 'h3[test]', - 'jupyter-book', + 'jupyter-book<2', 'sphinx>=7.3.3', # https://github.com/sphinx-doc/sphinx/issues/12290 'jupyterlab', 'jupyterlab-geojson', diff --git a/src/h3/__init__.py b/src/h3/__init__.py index 4f7cb32f..1e4f4b8d 100644 --- a/src/h3/__init__.py +++ b/src/h3/__init__.py @@ -26,4 +26,8 @@ H3MemoryAllocError, H3MemoryBoundsError, H3OptionInvalidError, + H3IndexInvalidError, + H3BaseCellDomainError, + H3DigitDomainError, + H3DeletedDigitError, ) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index 627f3b97..70fa087c 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -14,10 +14,13 @@ """ from .cells import ( + is_valid_index, is_valid_cell, is_pentagon, get_base_cell_number, get_resolution, + get_index_digit, + construct_cell, cell_to_parent, grid_distance, grid_disk, @@ -109,4 +112,11 @@ H3MemoryAllocError, H3MemoryBoundsError, H3OptionInvalidError, + H3IndexInvalidError, + H3BaseCellDomainError, + H3DigitDomainError, + H3DeletedDigitError, + + get_H3_ERROR_END, + error_code_to_exception, ) diff --git a/src/h3/_cy/cells.pxd b/src/h3/_cy/cells.pxd index 06b35ee6..1bc31b84 100644 --- a/src/h3/_cy/cells.pxd +++ b/src/h3/_cy/cells.pxd @@ -1,9 +1,12 @@ from .h3lib cimport bool, int64_t, H3int +cpdef bool is_valid_index(H3int h) cpdef bool is_valid_cell(H3int h) cpdef bool is_pentagon(H3int h) cpdef int get_base_cell_number(H3int h) except -1 cpdef int get_resolution(H3int h) except -1 +cpdef int get_index_digit(H3int h, int res) except -1 +cpdef H3int construct_cell(int baseCellNumber, const int[:] digits) except 0 cpdef int grid_distance(H3int h1, H3int h2) except -1 cpdef H3int[:] grid_disk(H3int h, int k) cpdef H3int[:] grid_ring(H3int h, int k) diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index 928b665c..78d25613 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -3,11 +3,12 @@ from .h3lib cimport bool, int64_t, H3int, H3ErrorCodes from .util cimport ( check_cell, - check_res, # we don't use? + check_index, check_distance, ) from .error_system cimport ( + H3Error, check_for_error, check_for_error_msg, ) @@ -19,6 +20,14 @@ from .memory cimport ( # todo: add notes about Cython exception handling +cpdef bool is_valid_index(H3int h): + """Validates an H3 index (cell, vertex, or directed edge). + + Returns + ------- + boolean + """ + return h3lib.isValidIndex(h) == 1 # bool is a python type, so we don't need the except clause cpdef bool is_valid_cell(H3int h): @@ -49,6 +58,33 @@ cpdef int get_resolution(H3int h) except -1: return h3lib.getResolution(h) +cpdef int get_index_digit(H3int h, int res) except -1: + cdef: + int digit + + check_index(h) + + check_for_error( + h3lib.getIndexDigit(h, res, &digit) + ) + + return digit + +cpdef H3int construct_cell(int base_cell_number, const int[:] digits) except 0: + cdef: + H3int out + int res = len(digits) + H3Error err + + if res > 0: + err = h3lib.constructCell(res, base_cell_number, &digits[0], &out) + else: + err = h3lib.constructCell(res, base_cell_number, NULL, &out) + + check_for_error(err) + + return out + cpdef int grid_distance(H3int h1, H3int h2) except -1: """ Compute the grid distance between two cells diff --git a/src/h3/_cy/error_system.pxd b/src/h3/_cy/error_system.pxd index af8c709b..4a1e3b45 100644 --- a/src/h3/_cy/error_system.pxd +++ b/src/h3/_cy/error_system.pxd @@ -1,3 +1,6 @@ from .h3lib cimport H3Error + +cpdef error_code_to_exception(H3Error err) cdef check_for_error(H3Error err) cdef check_for_error_msg(H3Error err, str msg) +cpdef H3Error get_H3_ERROR_END() diff --git a/src/h3/_cy/error_system.pyx b/src/h3/_cy/error_system.pyx index fbdd9185..fc18f467 100644 --- a/src/h3/_cy/error_system.pyx +++ b/src/h3/_cy/error_system.pyx @@ -60,6 +60,10 @@ Summarizing, all exceptions originating from the C library inherit from - H3MemoryAllocError - H3MemoryBoundsError - H3OptionInvalidError +- H3IndexInvalidError +- H3BaseCellDomainError +- H3DigitDomainError +- H3DeletedDigitError # TODO: add tests verifying that concrete exception classes have the right error codes associated with them @@ -87,6 +91,11 @@ from .h3lib cimport ( E_MEMORY_ALLOC, E_MEMORY_BOUNDS, E_OPTION_INVALID, + E_INDEX_INVALID, + E_BASE_CELL_DOMAIN, + E_DIGIT_DOMAIN, + E_DELETED_DIGIT, + H3_ERROR_END # sentinel value ) @contextmanager @@ -169,6 +178,10 @@ with _the_error(H3ValueError) as e: class H3NotNeighborsError(e): ... class H3ResMismatchError(e): ... class H3OptionInvalidError(e): ... + class H3IndexInvalidError(e): ... + class H3BaseCellDomainError(e): ... + class H3DigitDomainError(e): ... + class H3DeletedDigitError(e): ... """ @@ -192,6 +205,10 @@ error_mapping = { E_MEMORY_ALLOC: H3MemoryAllocError, E_MEMORY_BOUNDS: H3MemoryBoundsError, E_OPTION_INVALID: H3OptionInvalidError, + E_INDEX_INVALID: H3IndexInvalidError, + E_BASE_CELL_DOMAIN: H3BaseCellDomainError, + E_DIGIT_DOMAIN: H3DigitDomainError, + E_DELETED_DIGIT: H3DeletedDigitError, } # Go back and modify the class definitions so that each concrete exception @@ -207,25 +224,37 @@ for code, ex in error_mapping.items(): # TODO: Move the helpers to util? # TODO: Unclear how/where to expose these functions. cdef/cpdef? -cdef code_to_exception(H3Error err): +cpdef error_code_to_exception(H3Error err): + """ + Return Python exception corresponding to integer error code + given via the H3ErrorCodes enum in `h3api.h.in` in the C library. + """ if err == E_SUCCESS: return None elif err in error_mapping: return error_mapping[err] else: - raise UnknownH3ErrorCode(err) + return UnknownH3ErrorCode(err) cdef check_for_error(H3Error err): - ex = code_to_exception(err) + ex = error_code_to_exception(err) if ex: raise ex +cpdef H3Error get_H3_ERROR_END(): + """ + Return integer H3_ERROR_END from the H3ErrorCodes enum + in `h3api.h.in` in the C library, which is one greater than + the last valid error code. + """ + return H3_ERROR_END + # todo: There's no easy way to do `*args` in `cdef` functions, but I'm also # not sure this even needs to be a Cython `cdef` function at all, or that # any of the other helper functions need to be in Cython. # todo: Revisit after we've played with this a bit. # todo: also: maybe the extra messages aren't that much more helpful... cdef check_for_error_msg(H3Error err, str msg): - ex = code_to_exception(err) + ex = error_code_to_exception(err) if ex: raise ex(msg) diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index e94f655b..8cf74a7e 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -29,6 +29,11 @@ cdef extern from 'h3api.h': E_MEMORY_ALLOC = 13 E_MEMORY_BOUNDS = 14 E_OPTION_INVALID = 15 + E_INDEX_INVALID = 16 + E_BASE_CELL_DOMAIN = 17 + E_DIGIT_DOMAIN = 18 + E_DELETED_DIGIT = 19 + H3_ERROR_END # sentinel value ctypedef struct LatLng: double lat # in radians @@ -71,12 +76,15 @@ cdef extern from 'h3api.h': int isResClassIII(H3int h) nogil int isValidDirectedEdge(H3int edge) nogil int isValidVertex(H3int v) nogil + int isValidIndex(H3int h) nogil double degsToRads(double degrees) nogil double radsToDegs(double radians) nogil int getResolution(H3int h) nogil int getBaseCellNumber(H3int h) nogil + H3Error getIndexDigit(H3int h, int res, int *out) nogil + H3Error constructCell(int res, int baseCellNumber, const int *digits, H3int *out) nogil H3Error latLngToCell(const LatLng *g, int res, H3int *out) nogil H3Error cellToLatLng(H3int h, LatLng *) nogil diff --git a/src/h3/_cy/util.pxd b/src/h3/_cy/util.pxd index 4d7d962c..b584d3aa 100644 --- a/src/h3/_cy/util.pxd +++ b/src/h3/_cy/util.pxd @@ -8,6 +8,7 @@ cpdef H3str int_to_str(H3int x) cdef check_cell(H3int h) cdef check_edge(H3int e) -cdef check_res(int res) cdef check_vertex(H3int v) +cdef check_index(H3int h) +cdef check_res(int res) cdef check_distance(int k) diff --git a/src/h3/_cy/util.pyx b/src/h3/_cy/util.pyx index 7b8a9f20..9b070f9c 100644 --- a/src/h3/_cy/util.pyx +++ b/src/h3/_cy/util.pyx @@ -1,4 +1,11 @@ -from .h3lib cimport H3int, H3str, isValidCell, isValidDirectedEdge, isValidVertex +from .h3lib cimport ( + H3int, + H3str, + isValidCell, + isValidDirectedEdge, + isValidVertex, + isValidIndex, +) cimport h3lib @@ -7,7 +14,8 @@ from .error_system import ( H3DomainError, H3DirEdgeInvalidError, H3CellInvalidError, - H3VertexInvalidError + H3VertexInvalidError, + H3IndexInvalidError, ) cdef h3lib.LatLng deg2coord(double lat, double lng) nogil: @@ -78,6 +86,10 @@ cdef check_vertex(H3int v): if isValidVertex(v) == 0: raise H3VertexInvalidError('Integer is not a valid H3 vertex: {}'.format(hex(v))) +cdef check_index(H3int h): + if isValidIndex(h) == 0: + raise H3IndexInvalidError('Integer is not a valid H3 index: {}'.format(hex(h))) + cdef check_res(int res): if (res < 0) or (res > 15): raise H3ResDomainError(res) diff --git a/src/h3/api/basic_int/__init__.py b/src/h3/api/basic_int/__init__.py index 09f7deb8..47f1c7ec 100644 --- a/src/h3/api/basic_int/__init__.py +++ b/src/h3/api/basic_int/__init__.py @@ -1,5 +1,6 @@ # This file is **symlinked** across the APIs to ensure they are exactly the same. from typing import Literal +from array import array from ... import _cy from ..._h3shape import ( @@ -128,6 +129,20 @@ def average_hexagon_edge_length(res, unit='km'): return _cy.average_hexagon_edge_length(res, unit) +def is_valid_index(h): + """Validates *any* H3 index (cell, vertex, or directed edge). + + Returns + ------- + bool + """ + try: + h = _in_scalar(h) + return _cy.is_valid_index(h) + except (ValueError, TypeError): + return False + + def is_valid_cell(h): """ Validates an H3 cell (hexagon or pentagon). @@ -736,10 +751,124 @@ def get_base_cell_number(h): Returns ------- int + + Examples + -------- + >>> h = construct_cell(57, 2, 1, 4) + >>> h + '83728cfffffffff' + >> get_base_cell_number(h) + 57 """ return _cy.get_base_cell_number(_in_scalar(h)) +def get_index_digit(h, res): + """ + Get the index digit of a cell at the given resolution. + + Parameters + ---------- + h : H3Cell + Cell whose index digit will be returned. + res : int + Resolution (``>= 1``) at which to read the digit. + + Returns + ------- + int + The index digit at the requested resolution. + + Examples + -------- + >>> h = construct_cell(7, 2, 1, 4) + >>> h + '830e8cfffffffff' + >>> get_index_digit(h, 1) + 2 + >>> get_index_digit(h, 2) + 1 + >>> get_index_digit(h, 3) + 4 + """ + return _cy.get_index_digit(_in_scalar(h), res) + + +def construct_cell(base_cell_number, *digits, res=None): + """ + Construct cell from base cell and digits. + + Parameters + ---------- + base_cell_number : int + Base cell *number* (``0`` to ``121``). + *digits : int + Sequence of index digits (``0`` to ``6``). + Length of digits will be the resulting resolution of the output cell. + res : int, optional + Resolution of the constructed cell. If provided, it must equal + ``len(digits)``; otherwise it is inferred from the number of digits. + + Returns + ------- + H3Cell + The constructed cell. + + Examples + -------- + >>> construct_cell(7, 2, 1, 4) # resolution 3 cell + '830e8cfffffffff' + + >>> construct_cell(15, 0, 0, 5, 3) # resolution 4 cell + '841e057ffffffff' + + >>> construct_cell(15, 0, 0, 5, 3, res=4) + '841e057ffffffff' + """ + if (res is not None) and (len(digits) != res): + raise ValueError('Resolution must match number of digits.') + + digits = array('i', digits) + o = _cy.construct_cell(base_cell_number, digits) + return _out_scalar(o) + + +def deconstruct_cell(h): + """ + Deconstruct cell into base cell and digits. + + Parameters + ---------- + h : H3Cell + Cell to deconstruct. + + Returns + ------- + list of int + [base_cell_number, digit1, digit2, ..., digitN] + + Examples + -------- + >>> h = construct_cell(7, 2, 1, 4) # resolution 3 cell + >>> h + '830e8cfffffffff' + >>> deconstruct_cell(h) + (7, 2, 1, 4) + + >>> h = construct_cell(15, 0, 0, 5, 3) # resolution 4 cell + >>> h + '841e057ffffffff' + >>> deconstruct_cell(h) + (15, 0, 0, 5, 3) + >>> construct_cell(*deconstruct_cell(h), 0) == cell_to_center_child(h) + """ + res = get_resolution(h) + bc = get_base_cell_number(h) + digits = [get_index_digit(h, r + 1) for r in range(res)] + + return [bc, *digits] + + def are_neighbor_cells(h1, h2): """ Returns ``True`` if ``h1`` and ``h2`` are neighboring cells. diff --git a/src/h3lib b/src/h3lib index f6d1b0ba..70d4c9e5 160000 --- a/src/h3lib +++ b/src/h3lib @@ -1 +1 @@ -Subproject commit f6d1b0ba0a0eb076abe655ffdd576a445373a39e +Subproject commit 70d4c9e5007d74d77714afa44d5a2e52606828bf diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index f918019a..c4615829 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -8,6 +8,10 @@ H3ResMismatchError, H3CellInvalidError, H3NotNeighborsError, + H3DigitDomainError, + H3BaseCellDomainError, + H3DeletedDigitError, + H3IndexInvalidError, ) @@ -597,3 +601,153 @@ def test_to_local_ij_self(): out = h3.cell_to_local_ij(h, h) assert out == (-858, -2766) + + +def test_is_valid_index(): + assert h3.is_valid_index('8001fffffffffff') + assert not h3.is_valid_index('8a28308280fffff') + assert not h3.is_valid_index(123) + assert not h3.is_valid_index('abcd') + + +def test_is_valid_index_2(): + import h3.api.basic_int as h3 + + h = h3.latlng_to_cell(0,0,9) + assert h3.is_valid_index(h) + assert not h3.is_valid_index(h + 1) + assert not h3.is_valid_directed_edge(h) + assert not h3.is_valid_vertex(h) + + e = h3.origin_to_directed_edges(h)[0] + assert h3.is_valid_index(e) + assert not h3.is_valid_index(e + 1) + assert not h3.is_valid_cell(e) + assert not h3.is_valid_vertex(e) + + v = h3.cell_to_vertex(h, 0) + assert h3.is_valid_index(v) + assert not h3.is_valid_index(v + 1) + assert not h3.is_valid_cell(v) + assert not h3.is_valid_directed_edge(v) + + +def test_get_index_digit(): + assert h3.get_index_digit('822377fffffffff', 1) == 5 + assert h3.get_index_digit('822377fffffffff', 2) == 6 + assert h3.get_index_digit('822377fffffffff', 3) == 7 + assert h3.get_index_digit('822377fffffffff', 15) == 7 + + with pytest.raises(H3ResDomainError): + h3.get_index_digit('822377fffffffff', 16) + + with pytest.raises(H3ResDomainError): + h3.get_index_digit('822377fffffffff', 0) + + with pytest.raises(H3ResDomainError): + h3.get_index_digit('822377fffffffff',-1) + + +def test_get_index_digit_non_cells(): + h = h3.construct_cell(111, 3, 6, 4) + assert h == '83def4fffffffff' + + e = h3.origin_to_directed_edges(h)[0] + assert e == '113def4fffffffff' + assert h3.is_valid_directed_edge(e) + assert h3.get_index_digit(e, 1) == 3 + assert h3.get_index_digit(e, 2) == 6 + assert h3.get_index_digit(e, 3) == 4 + + e = h3.int_to_str(1 + h3.str_to_int(e)) + assert not h3.is_valid_directed_edge(e) + assert not h3.is_valid_index(e) + with pytest.raises(H3IndexInvalidError): + h3.get_index_digit(e, 1) + + v = h3.cell_to_vertexes(h)[0] + assert v == '223de1afffffffff' + assert h3.is_valid_vertex(v) + assert h3.get_index_digit(v, 1) == 0 + assert h3.get_index_digit(v, 2) == 3 + assert h3.get_index_digit(v, 3) == 2 + + v = h3.int_to_str(1 + h3.str_to_int(v)) + assert not h3.is_valid_vertex(v) + assert not h3.is_valid_index(v) + with pytest.raises(H3IndexInvalidError): + h3.get_index_digit(v, 1) + + +def test_construct_cell(): + bc = 17 + assert h3.construct_cell(bc, 5, 6) == '822377fffffffff' + assert h3.construct_cell(bc) == '8023fffffffffff' + + digits = [0] * 15 + assert h3.construct_cell(bc, *digits) == '8f2200000000000' + + with pytest.raises(H3ResDomainError): + digits = [0] * 16 + h3.construct_cell(bc, *digits) + + with pytest.raises(ValueError): + digits = [1,2,3,4] + h3.construct_cell(bc, *digits, res=2) + + with pytest.raises(H3DigitDomainError): + h3.construct_cell(121, 1, 7) # 7 is not a valid digit + + with pytest.raises(H3DigitDomainError): + h3.construct_cell(121, 45) # 45 is not a valid digit + + with pytest.raises(H3DigitDomainError): + h3.construct_cell(121, -1) # -1 is not a valid digit + + with pytest.raises(H3BaseCellDomainError): + h3.construct_cell(122) # one past last base cell number + + with pytest.raises(H3DeletedDigitError): + # 4 is a pentagon base cell. first nonzero digit can't be a 1 + h3.construct_cell(4, 1) + + with pytest.raises(H3DeletedDigitError): + # 4 is a pentagon base cell. first nonzero digit can't be a 1 + h3.construct_cell(4, 0,0,0, 0, 1) + + +def test_deconstruct_cell(): + h = '822377fffffffff' + assert h3.deconstruct_cell(h) == [17, 5, 6] + + +def test_construct_cell_inverses(): + # demonstrate functions are inverses of each other + + h = '8ff3ac688d63446' + components = [121, 6, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6] + + assert h3.construct_cell(*components) == h + assert h3.deconstruct_cell(h) == components + + assert h3.construct_cell(*h3.deconstruct_cell(h)) == h + assert h3.deconstruct_cell(h3.construct_cell(*components)) == components + + +def test_construct_cell_inverses_int_api(): + # same inverse test as above, but in int api + import h3.api.basic_int as h3 + + h = 0x8ff3ac688d63446 + components = [121, 6, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6] + + assert h3.construct_cell(*components) == h + assert h3.deconstruct_cell(h) == components + + assert h3.construct_cell(*h3.deconstruct_cell(h)) == h + assert h3.deconstruct_cell(h3.construct_cell(*components)) == components + + +def test_center_child_with_deconstruct(): + for h in h3.get_res0_cells(): + h3.construct_cell(*h3.deconstruct_cell(h), 0) == h3.cell_to_center_child(h) diff --git a/tests/test_lib/test_error_codes.py b/tests/test_lib/test_error_codes.py index 9961178d..4ae1d375 100644 --- a/tests/test_lib/test_error_codes.py +++ b/tests/test_lib/test_error_codes.py @@ -1,6 +1,10 @@ import pytest import h3 +from h3._cy import ( + error_code_to_exception, + get_H3_ERROR_END, +) # todo: maybe check the `check_for_error` function behavior directly? @@ -12,24 +16,30 @@ h3.H3GridNavigationError: None, h3.H3MemoryError: None, h3.H3ValueError: None, - - h3.H3FailedError: 1, - h3.H3DomainError: 2, - h3.H3LatLngDomainError: 3, - h3.H3ResDomainError: 4, - h3.H3CellInvalidError: 5, - h3.H3DirEdgeInvalidError: 6, - h3.H3UndirEdgeInvalidError: 7, - h3.H3VertexInvalidError: 8, - h3.H3PentagonError: 9, - h3.H3DuplicateInputError: 10, - h3.H3NotNeighborsError: 11, - h3.H3ResMismatchError: 12, - h3.H3MemoryAllocError: 13, - h3.H3MemoryBoundsError: 14, - h3.H3OptionInvalidError: 15, } +for e in range(1, get_H3_ERROR_END()): + ex = error_code_to_exception(e) + h3_exceptions[ex] = e + + +def test_num_error_codes(): + assert get_H3_ERROR_END() >= 20 + assert error_code_to_exception(19) == h3.H3DeletedDigitError + + # H3_ERROR_END (and beyond) shouldn't be a valid error code + code = get_H3_ERROR_END() + assert isinstance( + error_code_to_exception(code), + h3.UnknownH3ErrorCode + ) + + code = get_H3_ERROR_END() + 1 + assert isinstance( + error_code_to_exception(code), + h3.UnknownH3ErrorCode + ) + def test_error_codes_match(): """