From a684e5a249da28271ad41309458103e18ef0493a Mon Sep 17 00:00:00 2001 From: Jack Lovell Date: Wed, 29 Jul 2026 12:09:19 -0400 Subject: [PATCH] Make constants in cherab.core.utility.constants accessible to Python This enables single-sourcing constant values in both Cython code and Python code (including tests). Fixes #509. --- CHANGELOG.md | 1 + cherab/core/utility/constants.pyx | 49 ++++++++++++ cherab/core/utility/tests/test_constants.py | 84 +++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 cherab/core/utility/tests/test_constants.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e1071975..04ef3fc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ New: * Add Integrator2D base class for integration of two-dimensional functions. (#472) * Support Raysect 0.9. (#486) * Test against Python 3.9, 3.10, 3.11, 3.12, 3.13 and latest released Numpy. Drop Python 3.7, 3.8 and older Numpy from tests. (#486) +* Make values in `cherab.core.utility.constants` accessible to Python. (#509) Release 1.5.0 (27 Aug 2024) ------------------- diff --git a/cherab/core/utility/constants.pyx b/cherab/core/utility/constants.pyx index 423d15e8..18f94a7d 100644 --- a/cherab/core/utility/constants.pyx +++ b/cherab/core/utility/constants.pyx @@ -15,6 +15,11 @@ # # See the Licence for the specific language governing permissions and limitations # under the Licence. +import sys +from types import ModuleType + +from libc.math cimport M_PI + cdef: @@ -35,3 +40,47 @@ cdef: double RYDBERG_CONSTANT_EV = 13.605693122994 double VACUUM_PERMITTIVITY = 8.8541878128e-12 double BOHR_MAGNETON = 5.78838180123e-5 # in eV/T + + +# Make the constants available to Python too. +# To ensure the Python and Cython constants do not got out of sync the exported +# Python attributes of the module are made read only using module getattr. +cdef dict _CONSTANTS = { + # c stdlib + "RECIP_2_PI": RECIP_2_PI, + "RECIP_4_PI": RECIP_4_PI, + "DEGREES_TO_RADIANS": DEGREES_TO_RADIANS, + "RADIANS_TO_DEGREES": RADIANS_TO_DEGREES, + # NIST 2018 + "ATOMIC_MASS": ATOMIC_MASS, + "ELEMENTARY_CHARGE": ELEMENTARY_CHARGE, + "SPEED_OF_LIGHT": SPEED_OF_LIGHT, + "PLANCK_CONSTANT": PLANCK_CONSTANT, + "HC_EV_NM": HC_EV_NM, + "ELECTRON_CLASSICAL_RADIUS": ELECTRON_CLASSICAL_RADIUS, + "ELECTRON_REST_MASS": ELECTRON_REST_MASS, + "RYDBERG_CONSTANT_EV": RYDBERG_CONSTANT_EV, + "VACUUM_PERMITTIVITY": VACUUM_PERMITTIVITY, + "BOHR_MAGNETON": BOHR_MAGNETON, +} + + +def __getattr__(name): + if name not in _CONSTANTS: + raise AttributeError() + return _CONSTANTS[name] + + +def __dir__(): + return list(_CONSTANTS.keys()) + + +class ReadOnlyModule(ModuleType): + def __setattr__(self, attr, value): + raise AttributeError("Constants are read-only") + + def __delattr__(self, attr): + raise AttributeError("Constants are read-only") + + +sys.modules[__name__].__class__ = ReadOnlyModule diff --git a/cherab/core/utility/tests/test_constants.py b/cherab/core/utility/tests/test_constants.py new file mode 100644 index 00000000..9d46c0d2 --- /dev/null +++ b/cherab/core/utility/tests/test_constants.py @@ -0,0 +1,84 @@ +# Copyright 2026 Oak Ridge National Laboratory +# +# Licensed under the EUPL, Version 1.1 or – as soon they will be approved by the +# European Commission - subsequent versions of the EUPL (the "Licence"); +# You may not use this work except in compliance with the Licence. +# You may obtain a copy of the Licence at: +# +# https://joinup.ec.europa.eu/software/page/eupl5 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. +# +# See the Licence for the specific language governing permissions and limitations +# under the Licence. + +import math +import unittest +from cherab.core.utility import constants + + +class TestConstants(unittest.TestCase): + def setUp(self): + self._expected_constants = dict( + # sourced c standard maths library + # CPython wraps libc's math so uses the same constants as cython's + # cimport of libc.math. + RECIP_2_PI=1 / (2 * math.pi), + RECIP_4_PI=1 / (4 * math.pi), + DEGREES_TO_RADIANS=math.pi / 180, + RADIANS_TO_DEGREES=180 / math.pi, + + # sourced from NIST, CODATA 2018=https://physics.nist.gov/cuu/Constants/Table/allascii.txt + ATOMIC_MASS=1.66053906660e-27, + ELEMENTARY_CHARGE=1.602176634e-19, + SPEED_OF_LIGHT=299792458.0, + PLANCK_CONSTANT=6.62607015e-34, + HC_EV_NM=1239.8419738620933, # (Planck constant in eV s) x (speed of light in nm/s) + ELECTRON_CLASSICAL_RADIUS=2.8179403262e-15, + ELECTRON_REST_MASS=9.1093837015e-31, + RYDBERG_CONSTANT_EV=13.605693122994, + VACUUM_PERMITTIVITY=8.8541878128e-12, + BOHR_MAGNETON=5.78838180123e-5, # in eV/T + ) + + def test_all_exported(self): + """ + Test Cython constants exported as Python floats. + """ + for name, value in self._expected_constants.items(): + self.assertEqual(value, getattr(constants, name)) + + def test_exported_literal(self): + """ + Test a constant accessed by literal name. + """ + self.assertEqual(self._expected_constants['ATOMIC_MASS'], constants.ATOMIC_MASS) + + def test_exported_names(self): + """ + Test all exported names are as expected by this test class. + """ + self.assertEqual(sorted(self._expected_constants.keys()), sorted(dir(constants))) + + def test_readonly(self): + """ + Test that constants can't be modified or removed and new constants can't be added. + """ + with self.assertRaises(AttributeError): + constants.ATOMIC_MASS = 1.66e-27 + + def test_nonew(self): + """ + Test that attempting to assign a new constant from Python errors. + """ + with self.assertRaises(AttributeError): + constants.TAU = math.tau + + def test_nodel(self): + """ + Test that attempting to delete and constant from Python errors. + """ + with self.assertRaises(AttributeError): + del constants.RYDBERG_CONSTANT_EV