Skip to content

Make values in cherab.core.utility.constants accessible from Python #509

Description

@jacklovell

Currently these values (atomic mass, elementary charge etc) are only accessible in extension classes using cimport. It would be valuable to be able to import them in Python files as well so we can single-source constants, for example in tests.

In general it is difficult to properly shadow C types defined in Cython with Python types (cython/cython#3959 (comment)), but in the simple case of a collection of constants all with the same type we can make use of ctypes, module-level __getattr__ and a custom module class (https://docs.python.org/3/reference/datamodel.html#customizing-module-attribute-access).

Proof-of-concept:

import ctypes
import sys
from types import ModuleType
from cherab.core.utility import constants


PyCapsule_GetName = ctypes.pythonapi.PyCapsule_GetName
PyCapsule_GetName.restype = ctypes.c_char_p
PyCapsule_GetName.argtypes = [ctypes.py_object]
PyCapsule_GetPointer = ctypes.pythonapi.PyCapsule_GetPointer
PyCapsule_GetPointer.restype = ctypes.c_void_p
PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p]
PyFloat_FromDouble = ctypes.pythonapi.PyFloat_FromDouble
PyFloat_FromDouble.restype = ctypes.py_object
PyFloat_FromDouble.argtypes = [ctypes.c_double]

DoublePtr = ctypes.POINTER(ctypes.c_double)


def __getattr__(name):
    if name not in constants.__pyx_capi__:
        raise AttributeError()
    capsule = constants.__pyx_capi__[name]
    capsule_name =  PyCapsule_GetName(capsule)
    voidptr = PyCapsule_GetPointer(capsule, capsule_name)
    doubleptr = ctypes.cast(voidptr, DoublePtr)
    val = PyFloat_FromDouble(doubleptr.contents)
    return val


def __dir__():
    return list(constants.__pyx_capi__)


class ReadOnlyModule(ModuleType):
    def __setattr__(self, attr, value):
        raise ValueError("Can't add/modify constants.")


sys.modules[__name__].__class__ = ReadOnlyModule

Leads to some nice usability improvements (auto-complete and module help for example):

(cherab-dev) 3jl@fusion3:~$ python
Python 3.10.12 (main, Jun 22 2026, 18:55:27) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import cherab_utility_constants as cuc
>>> cuc.ATOMIC_MASS
1.6605390666e-27
>>> cuc.VACUUM_PERMITTIVITY
8.8541878128e-12
>>> cuc.VACUUM_PERMITTIVITY += 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/3jl/cherab_utility_constants.py", line 40, in __setattr__
    raise ValueError("Can't add/modify constants.")
ValueError: Can't add/modify constants.
>>> cuc.<TAB>
cuc.ATOMIC_MASS                cuc.ELEMENTARY_CHARGE          cuc.RECIP_4_PI
cuc.BOHR_MAGNETON              cuc.HC_EV_NM                   cuc.RYDBERG_CONSTANT_EV
cuc.DEGREES_TO_RADIANS         cuc.PLANCK_CONSTANT            cuc.SPEED_OF_LIGHT
cuc.ELECTRON_CLASSICAL_RADIUS  cuc.RADIANS_TO_DEGREES         cuc.VACUUM_PERMITTIVITY
cuc.ELECTRON_REST_MASS         cuc.RECIP_2_PI
>>> help(cuc)
Help on module cherab_utility_constants:

NAME
    cherab_utility_constants - Extract cdef double constants from cherab.core.utility for use in Python.

DATA
    ATOMIC_MASS = 1.6605390666e-27
    BOHR_MAGNETON = 5.78838180123e-05
    DEGREES_TO_RADIANS = 0.017453292519943295
    ELECTRON_CLASSICAL_RADIUS = 2.8179403262e-15
    ELECTRON_REST_MASS = 9.1093837015e-31
    ELEMENTARY_CHARGE = 1.602176634e-19
    HC_EV_NM = 1239.8419738620933
    PLANCK_CONSTANT = 6.62607015e-34
    RADIANS_TO_DEGREES = 57.29577951308232
    RECIP_2_PI = 0.15915494309189535
    RECIP_4_PI = 0.07957747154594767
    RYDBERG_CONSTANT_EV = 13.605693122994
    SPEED_OF_LIGHT = 299792458.0
    VACUUM_PERMITTIVITY = 8.8541878128e-12

FILE
    /home/3jl/cherab_utility_constants.py

Implementing __getattr__ and the custom ModuleType class within the pyx file itself would probably also allow the removal of a lot of the ctypes boilerplate, though I haven't tested this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions