Skip to content

Commit d3a6bf3

Browse files
authored
Error improvements (#138)
* making val_line_error a function * simplify context * simplify location * better error creation * cleanup * update context! macro * more cleanup * move error context into enum (#139) * move error context into enum * tweak * simplify error rendering * use static strings where possible * tweak error kind rendering * improve coverage * improve coverage * cleanup * remove unused code * correct imports * cleanup location logic
1 parent b95b3d2 commit d3a6bf3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+782
-771
lines changed

.pre-commit-config.yaml

-10
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,3 @@ repos:
2222
entry: make lint-rust
2323
types: [rust]
2424
language: system
25-
- id: pyright
26-
name: Pyright
27-
entry: make pyright
28-
types: [python]
29-
language: system
30-
- id: build
31-
name: Build
32-
entry: make build-dev
33-
types: [rust]
34-
language: system

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ testcov-windows: build-cov-windows test
105105

106106

107107
.PHONY: all
108-
all: format build-dev lint pyright test
108+
all: format build-dev lint test
109109

110110
.PHONY: flame
111111
flame:

src/build_tools.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use pyo3::prelude::*;
66
use pyo3::types::PyDict;
77
use pyo3::{FromPyObject, PyErrArguments};
88

9+
use crate::errors::{pretty_line_errors, ValError};
10+
911
pub trait SchemaDict<'py> {
1012
fn get_as<T>(&'py self, key: &str) -> PyResult<Option<T>>
1113
where
@@ -161,5 +163,4 @@ macro_rules! py_error {
161163
Err(<$error_type>::new_err(format!($msg, $( $msg_args ),+)))
162164
};
163165
}
164-
use crate::errors::{pretty_line_errors, ValError};
165166
pub(crate) use py_error;

src/errors/kinds.rs

+191-52
Large diffs are not rendered by default.

src/errors/line_error.rs

+31-119
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
use std::fmt;
2-
31
use pyo3::prelude::*;
4-
use pyo3::types::PyDict;
52

6-
use crate::input::JsonInput;
3+
use crate::input::{Input, JsonInput};
74

85
use super::kinds::ErrorKind;
96
use super::location::{LocItem, Location};
@@ -29,6 +26,16 @@ impl<'a> From<Vec<ValLineError<'a>>> for ValError<'a> {
2926
}
3027
}
3128

29+
impl<'a> ValError<'a> {
30+
pub fn new(kind: ErrorKind, input: &'a impl Input<'a>) -> ValError<'a> {
31+
Self::LineErrors(vec![ValLineError::new(kind, input)])
32+
}
33+
34+
pub fn new_with_loc(kind: ErrorKind, input: &'a impl Input<'a>, loc: impl Into<LocItem>) -> ValError<'a> {
35+
Self::LineErrors(vec![ValLineError::new_with_loc(kind, input, loc)])
36+
}
37+
}
38+
3239
// ValError used to implement Error, see #78 for removed code
3340

3441
// TODO, remove and replace with just .into()
@@ -44,20 +51,35 @@ pub fn pretty_line_errors(py: Python, line_errors: Vec<ValLineError>) -> String
4451
/// A `ValLineError` is a single error that occurred during validation which is converted to a `PyLineError`
4552
/// to eventually form a `ValidationError`.
4653
/// I don't like the name `ValLineError`, but it's the best I could come up with (for now).
47-
#[derive(Debug, Default)]
54+
#[derive(Debug)]
4855
pub struct ValLineError<'a> {
4956
pub kind: ErrorKind,
5057
// location is reversed so that adding an "outer" location item is pushing, it's reversed before showing to the user
51-
pub reverse_location: Location,
58+
pub location: Location,
5259
pub input_value: InputValue<'a>,
53-
pub context: Context,
5460
}
5561

5662
impl<'a> ValLineError<'a> {
63+
pub fn new(kind: ErrorKind, input: &'a impl Input<'a>) -> ValLineError<'a> {
64+
Self {
65+
kind,
66+
input_value: input.as_error_value(),
67+
location: Location::default(),
68+
}
69+
}
70+
71+
pub fn new_with_loc(kind: ErrorKind, input: &'a impl Input<'a>, loc: impl Into<LocItem>) -> ValLineError<'a> {
72+
Self {
73+
kind,
74+
input_value: input.as_error_value(),
75+
location: Location::new_some(loc.into()),
76+
}
77+
}
78+
5779
/// location is stored reversed so it's quicker to add "outer" items as that's what we always do
5880
/// hence `push` here instead of `insert`
5981
pub fn with_outer_location(mut self, loc_item: LocItem) -> Self {
60-
self.reverse_location.push(loc_item);
82+
self.location.with_outer(loc_item);
6183
self
6284
}
6385

@@ -70,28 +92,20 @@ impl<'a> ValLineError<'a> {
7092
pub fn into_new<'b>(self, py: Python) -> ValLineError<'b> {
7193
ValLineError {
7294
kind: self.kind,
73-
reverse_location: self.reverse_location,
95+
location: self.location,
7496
input_value: self.input_value.to_object(py).into(),
75-
context: self.context,
7697
}
7798
}
7899
}
79100

80101
#[derive(Debug)]
81102
pub enum InputValue<'a> {
82-
None,
83103
PyAny(&'a PyAny),
84104
JsonInput(&'a JsonInput),
85105
String(&'a str),
86106
PyObject(PyObject),
87107
}
88108

89-
impl Default for InputValue<'_> {
90-
fn default() -> Self {
91-
Self::None
92-
}
93-
}
94-
95109
impl<'a> From<PyObject> for InputValue<'a> {
96110
fn from(py_object: PyObject) -> Self {
97111
Self::PyObject(py_object)
@@ -101,112 +115,10 @@ impl<'a> From<PyObject> for InputValue<'a> {
101115
impl<'a> ToPyObject for InputValue<'a> {
102116
fn to_object(&self, py: Python) -> PyObject {
103117
match self {
104-
Self::None => py.None(),
105118
Self::PyAny(input) => input.into_py(py),
106119
Self::JsonInput(input) => input.to_object(py),
107120
Self::String(input) => input.into_py(py),
108121
Self::PyObject(py_obj) => py_obj.into_py(py),
109122
}
110123
}
111124
}
112-
113-
#[derive(Debug, Clone, Default)]
114-
pub struct Context(Vec<(String, ContextValue)>);
115-
116-
impl Context {
117-
pub fn new<I: IntoIterator<Item = (String, ContextValue)>>(raw: I) -> Self {
118-
Self(raw.into_iter().collect())
119-
}
120-
121-
pub fn is_empty(&self) -> bool {
122-
self.0.is_empty()
123-
}
124-
125-
pub fn render(&self, template: String) -> String {
126-
let mut rendered = template;
127-
for (key, value) in &self.0 {
128-
rendered = rendered.replace(&format!("{{{}}}", key), &value.to_string());
129-
}
130-
rendered
131-
}
132-
}
133-
134-
impl fmt::Display for Context {
135-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136-
let loc = self
137-
.0
138-
.iter()
139-
.map(|(k, v)| format!("{}: {}", k, v))
140-
.collect::<Vec<String>>()
141-
.join(", ");
142-
write!(f, "{{{}}}", loc)
143-
}
144-
}
145-
146-
// maybe this is overkill and we should just use fmt::Display an convert to string when creating Context?
147-
#[derive(Debug, Clone)]
148-
pub enum ContextValue {
149-
S(String),
150-
I(i64),
151-
F(f64),
152-
}
153-
154-
impl fmt::Display for ContextValue {
155-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156-
match self {
157-
ContextValue::S(v) => write!(f, "{}", v),
158-
ContextValue::I(v) => write!(f, "{}", v),
159-
ContextValue::F(v) => write!(f, "{}", v),
160-
}
161-
}
162-
}
163-
164-
impl From<String> for ContextValue {
165-
fn from(str: String) -> Self {
166-
Self::S(str)
167-
}
168-
}
169-
170-
impl From<&str> for ContextValue {
171-
fn from(str: &str) -> Self {
172-
Self::S(str.to_string())
173-
}
174-
}
175-
176-
impl From<i64> for ContextValue {
177-
fn from(int: i64) -> Self {
178-
Self::I(int)
179-
}
180-
}
181-
182-
impl From<usize> for ContextValue {
183-
fn from(u: usize) -> Self {
184-
Self::I(u as i64)
185-
}
186-
}
187-
188-
impl From<f64> for ContextValue {
189-
fn from(f: f64) -> Self {
190-
Self::F(f)
191-
}
192-
}
193-
194-
impl ToPyObject for ContextValue {
195-
fn to_object(&self, py: Python) -> PyObject {
196-
match self {
197-
ContextValue::S(v) => v.into_py(py),
198-
ContextValue::I(v) => v.into_py(py),
199-
ContextValue::F(v) => v.into_py(py),
200-
}
201-
}
202-
}
203-
204-
impl ToPyObject for Context {
205-
fn to_object(&self, py: Python) -> PyObject {
206-
let dict = PyDict::new(py);
207-
for (key, value) in &self.0 {
208-
dict.set_item(key, value).unwrap();
209-
}
210-
dict.into_py(py)
211-
}
212-
}

src/errors/location.rs

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::fmt;
22

33
use pyo3::prelude::*;
4+
use pyo3::types::PyList;
45

56
/// Used to store individual items of the error location, e.g. a string for key/field names
67
/// or a number for array indices.
@@ -51,4 +52,59 @@ impl ToPyObject for LocItem {
5152
/// Error locations are represented by a vector of `LocItem`s.
5253
/// e.g. if the error occurred in the third member of a list called `foo`,
5354
/// the location would be `["foo", 2]`.
54-
pub type Location = Vec<LocItem>;
55+
/// Note: location in List is stored in **REVERSE** so adding an "outer" item to location involves
56+
/// pushing to the vec which is faster than inserting and shifting everything along.
57+
/// Then when "using" location in `Display` and `ToPyObject` order has to be reversed
58+
#[derive(Debug, Clone)]
59+
pub enum Location {
60+
// no location, avoid creating an unnecessary vec
61+
Empty,
62+
// store the in a vec of LocItems, Note: this is the REVERSE of location, see above
63+
// we could perhaps use a smallvec or similar here, probably only worth it if we store a Cow in LocItem
64+
List(Vec<LocItem>),
65+
}
66+
67+
impl Default for Location {
68+
fn default() -> Self {
69+
Self::Empty
70+
}
71+
}
72+
73+
impl ToPyObject for Location {
74+
fn to_object(&self, py: Python<'_>) -> PyObject {
75+
match self {
76+
Self::List(loc) => loc.iter().rev().collect::<Vec<_>>().to_object(py),
77+
Self::Empty => PyList::empty(py).to_object(py),
78+
}
79+
}
80+
}
81+
82+
impl fmt::Display for Location {
83+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84+
match self {
85+
Self::List(loc) => {
86+
let loc_str = loc.iter().rev().map(|i| i.to_string()).collect::<Vec<_>>();
87+
writeln!(f, "{}", loc_str.join(" -> "))
88+
}
89+
Self::Empty => Ok(()),
90+
}
91+
}
92+
}
93+
94+
impl Location {
95+
/// create a new location vec with a value, 3 is plucked out of thin air, should it just be 1?
96+
pub fn new_some(item: LocItem) -> Self {
97+
let mut loc = Vec::with_capacity(3);
98+
loc.push(item);
99+
Self::List(loc)
100+
}
101+
102+
pub fn with_outer(&mut self, loc_item: LocItem) {
103+
match self {
104+
Self::List(ref mut loc) => loc.push(loc_item),
105+
Self::Empty => {
106+
*self = Self::new_some(loc_item);
107+
}
108+
};
109+
}
110+
}

src/errors/mod.rs

+3-35
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,14 @@ use pyo3::prelude::*;
22

33
mod kinds;
44
mod line_error;
5-
pub mod location;
5+
mod location;
66
mod validation_exception;
77

88
pub use self::kinds::ErrorKind;
9-
pub use self::line_error::{as_internal, pretty_line_errors, Context, InputValue, ValError, ValLineError, ValResult};
9+
pub use self::line_error::{as_internal, pretty_line_errors, InputValue, ValError, ValLineError, ValResult};
10+
pub use self::location::LocItem;
1011
pub use self::validation_exception::ValidationError;
1112

12-
/// Utility for concisely creating a `ValLineError`
13-
/// can either take just `py` and a `value` (the given value) in which case kind `ErrorKind::ValueError` is used as kind
14-
/// e.g. `val_line_error!(py, "the value provided")`
15-
/// or, `py`, `value` and a mapping of other attributes for `ValLineError`
16-
/// e.g. `val_line_error!(py, "the value provided", kind=ErrorKind::ExtraForbidden, message="the message")`
17-
macro_rules! val_line_error {
18-
($($key:ident = $val:expr),+ $(,)?) => {
19-
crate::errors::ValLineError {
20-
$(
21-
$key: $val,
22-
)+
23-
..Default::default()
24-
}
25-
};
26-
}
27-
pub(crate) use val_line_error;
28-
29-
/// Utility for concisely creating a `Err(ValError::LineErrors([?]))` containing a single `ValLineError`
30-
/// Usage matches `val_line_error`
31-
macro_rules! err_val_error {
32-
($($key:ident = $val:expr),+ $(,)?) => {
33-
Err(crate::errors::ValError::LineErrors(vec![crate::errors::val_line_error!($($key = $val),+)]))
34-
};
35-
}
36-
pub(crate) use err_val_error;
37-
38-
macro_rules! context {
39-
($($k:expr => $v:expr),* $(,)?) => {{
40-
crate::errors::Context::new([$(($k.into(), $v.into()),)*])
41-
}};
42-
}
43-
pub(crate) use context;
44-
4513
pub fn py_err_string(py: Python, err: PyErr) -> String {
4614
let value = err.value(py);
4715
match value.get_type().name() {

0 commit comments

Comments
 (0)