Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ h3c/
.cache/
h3/out/
.ruff_cache/
_build/

# Generated C code from Cython test
tests/**/*.c
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ avoid adding features or APIs which do not map onto the

## Unreleased

None.
- Add new error codes and helper functions

## [4.3.1] - 2025-08-10

Expand Down
4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# REMOVE BEFORE LANDING
test:
make init
make test
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
Expand Down Expand Up @@ -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',
Expand Down
7 changes: 7 additions & 0 deletions src/h3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,11 @@
H3MemoryAllocError,
H3MemoryBoundsError,
H3OptionInvalidError,
H3IndexInvalidError,
H3BaseCellDomainError,
H3DigitDomainError,
H3DeletedDigitError,

get_H3_ERROR_END,
error_code_to_exception,
)
8 changes: 8 additions & 0 deletions src/h3/_cy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""

from .cells import (
is_valid_index,
is_valid_cell,
is_pentagon,
get_base_cell_number,
Expand Down Expand Up @@ -109,4 +110,11 @@
H3MemoryAllocError,
H3MemoryBoundsError,
H3OptionInvalidError,
H3IndexInvalidError,
H3BaseCellDomainError,
H3DigitDomainError,
H3DeletedDigitError,

get_H3_ERROR_END,
error_code_to_exception,
)
1 change: 1 addition & 0 deletions src/h3/_cy/cells.pxd
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
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
Expand Down
8 changes: 8 additions & 0 deletions src/h3/_cy/cells.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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):
Expand Down
3 changes: 3 additions & 0 deletions src/h3/_cy/error_system.pxd
Original file line number Diff line number Diff line change
@@ -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()
37 changes: 33 additions & 4 deletions src/h3/_cy/error_system.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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): ...


"""
Expand All @@ -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
Expand All @@ -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)
6 changes: 6 additions & 0 deletions src/h3/_cy/h3lib.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -71,6 +76,7 @@ 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
Expand Down
14 changes: 14 additions & 0 deletions src/h3/api/basic_int/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ def average_hexagon_edge_length(res, unit='km'):
return _cy.average_hexagon_edge_length(res, unit)


def is_valid_index(h):
"""Validates an 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).
Expand Down
7 changes: 7 additions & 0 deletions tests/test_lib/test_cells_and_edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,3 +597,10 @@ 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')
38 changes: 22 additions & 16 deletions tests/test_lib/test_error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,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, h3.get_H3_ERROR_END()):
ex = h3.error_code_to_exception(e)
h3_exceptions[ex] = e


def test_num_error_codes():
assert h3.get_H3_ERROR_END() >= 20
assert h3.error_code_to_exception(19) == h3.H3DeletedDigitError

# H3_ERROR_END (and beyond) shouldn't be a valid error code
code = h3.get_H3_ERROR_END()
assert isinstance(
h3.error_code_to_exception(code),
h3.UnknownH3ErrorCode
)

code = h3.get_H3_ERROR_END() + 1
assert isinstance(
h3.error_code_to_exception(code),
h3.UnknownH3ErrorCode
)


def test_error_codes_match():
"""
Expand Down
Loading