Skip to content

Commit a48d457

Browse files
Merge pull request #301 from nyx-space/245-improve-error-handling-bis
Support exceptions in Python
2 parents c88a3a0 + 0ac2215 commit a48d457

File tree

4 files changed

+85
-28
lines changed

4 files changed

+85
-28
lines changed

.github/workflows/python.yml

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ jobs:
126126
- uses: actions/checkout@v4
127127
- uses: actions/setup-python@v5
128128
with:
129-
python-version: '3.10'
129+
python-version: '3.11'
130130
- name: Build wheels
131131
uses: PyO3/maturin-action@v1
132132
with:
@@ -138,14 +138,6 @@ jobs:
138138
with:
139139
name: wheels-macos-${{ matrix.platform.target }}
140140
path: dist
141-
- name: pytest
142-
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
143-
shell: bash
144-
run: |
145-
set -e
146-
pip install hifitime --find-links dist --force-reinstall --no-index -vv
147-
pip install pytest
148-
pytest
149141

150142
sdist:
151143
runs-on: ubuntu-latest

src/duration/kani_verif.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ fn formal_duration_truncated_ns_reciprocity() {
4343
// Then it does not fit on a i64, so this function should return an error
4444
assert_eq!(
4545
dur_from_part.try_truncated_nanoseconds(),
46-
Err(DurationError::Overflow)
46+
Err(Err(EpochError::Duration {
47+
source: DurationError::Overflow,
48+
}))
4749
);
4850
} else if centuries == -1 {
4951
// If we are negative by just enough that the centuries is negative, then the truncated seconds

src/python.rs

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,82 @@
88
* Documentation: https://nyxspace.com/
99
*/
1010

11-
use pyo3::{exceptions::PyException, prelude::*};
11+
use pyo3::{
12+
exceptions::{PyBaseException, PyException},
13+
prelude::*,
14+
types::{PyDict, PyTuple},
15+
};
1216

1317
use crate::leap_seconds::{LatestLeapSeconds, LeapSecondsFile};
1418
use crate::prelude::*;
1519
use crate::ut1::Ut1Provider;
1620

21+
// Keep the module at the top
22+
#[pymodule]
23+
fn hifitime(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
24+
m.add_class::<Epoch>()?;
25+
m.add_class::<TimeScale>()?;
26+
m.add_class::<TimeSeries>()?;
27+
m.add_class::<Duration>()?;
28+
m.add_class::<Unit>()?;
29+
m.add_class::<LatestLeapSeconds>()?;
30+
m.add_class::<LeapSecondsFile>()?;
31+
m.add_class::<Ut1Provider>()?;
32+
m.add_class::<PyEpochError>()?;
33+
m.add_class::<PyDurationError>()?;
34+
m.add_class::<PyParsingError>()?;
35+
Ok(())
36+
}
37+
38+
#[pyclass]
39+
#[pyo3(name = "EpochError", extends = PyBaseException)]
40+
pub struct PyEpochError {}
41+
42+
#[pymethods]
43+
impl PyEpochError {
44+
#[new]
45+
#[pyo3(signature = (*_args, **_kwargs))]
46+
fn new(_args: Bound<'_, PyTuple>, _kwargs: Option<Bound<'_, PyDict>>) -> Self {
47+
Self {}
48+
}
49+
}
50+
51+
#[pyclass]
52+
#[pyo3(name = "ParsingError", extends = PyBaseException)]
53+
pub struct PyParsingError {}
54+
55+
#[pymethods]
56+
impl PyParsingError {
57+
#[new]
58+
#[pyo3(signature = (*_args, **_kwargs))]
59+
fn new(_args: Bound<'_, PyTuple>, _kwargs: Option<Bound<'_, PyDict>>) -> Self {
60+
Self {}
61+
}
62+
}
63+
64+
#[pyclass]
65+
#[pyo3(name = "DurationError", extends = PyBaseException)]
66+
pub struct PyDurationError {}
67+
68+
#[pymethods]
69+
impl PyDurationError {
70+
#[new]
71+
#[pyo3(signature = (*_args, **_kwargs))]
72+
fn new(_args: Bound<'_, PyTuple>, _kwargs: Option<Bound<'_, PyDict>>) -> Self {
73+
Self {}
74+
}
75+
}
76+
77+
// convert you library error into a PyErr using the custom exception type
1778
impl From<EpochError> for PyErr {
18-
fn from(err: EpochError) -> PyErr {
19-
PyException::new_err(err.to_string())
79+
fn from(err: EpochError) -> Self {
80+
PyErr::new::<PyEpochError, _>(err.to_string())
2081
}
2182
}
2283

2384
impl From<ParsingError> for PyErr {
2485
fn from(err: ParsingError) -> PyErr {
25-
PyException::new_err(err.to_string())
86+
PyErr::new::<PyParsingError, _>(err.to_string())
2687
}
2788
}
2889

@@ -31,16 +92,3 @@ impl From<DurationError> for PyErr {
3192
PyException::new_err(err.to_string())
3293
}
3394
}
34-
35-
#[pymodule]
36-
fn hifitime(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
37-
m.add_class::<Epoch>()?;
38-
m.add_class::<TimeScale>()?;
39-
m.add_class::<TimeSeries>()?;
40-
m.add_class::<Duration>()?;
41-
m.add_class::<Unit>()?;
42-
m.add_class::<LatestLeapSeconds>()?;
43-
m.add_class::<LeapSecondsFile>()?;
44-
m.add_class::<Ut1Provider>()?;
45-
Ok(())
46-
}

tests/python/test_epoch.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from hifitime import Epoch, TimeSeries, Unit, Duration
1+
from hifitime import Duration, Epoch, EpochError, ParsingError, TimeSeries, Unit
22
from datetime import datetime
33
import pickle
44

@@ -19,6 +19,13 @@ def test_strtime():
1919

2020
assert pickle.loads(pickle.dumps(epoch)) == epoch
2121

22+
try:
23+
epoch.strftime("%o")
24+
except ParsingError as e:
25+
print(f"caught {e}")
26+
else:
27+
raise AssertionError("failed to catch parsing error")
28+
2229

2330
def test_utcnow():
2431
epoch = Epoch.system_now()
@@ -67,3 +74,11 @@ def test_duration_eq():
6774

6875
dur = Duration("37 min 26 s")
6976
assert pickle.loads(pickle.dumps(dur)) == dur
77+
78+
def test_exceptions():
79+
try:
80+
Epoch("invalid")
81+
except EpochError as e:
82+
print(f"caught {e}")
83+
else:
84+
raise AssertionError("failed to catch epoch error")

0 commit comments

Comments
 (0)