Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
728 changes: 548 additions & 180 deletions Cargo.lock

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,17 @@ lto = false

[workspace.dependencies]
# ruff, ty and related crates
ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", package = "ruff_python_parser", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ruff_python_stdlib = { git = "https://github.com/astral-sh/ruff.git", package = "ruff_python_stdlib", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", package = "ruff_python_ast", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ruff_text_size = { git = "https://github.com/astral-sh/ruff.git", package = "ruff_text_size", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ruff_db = { git = "https://github.com/astral-sh/ruff.git", package = "ruff_db", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d", features = ["serde"] }
ty_python_semantic = { git = "https://github.com/astral-sh/ruff.git", package = "ty_python_semantic", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ty_module_resolver = { git = "https://github.com/astral-sh/ruff.git", package = "ty_module_resolver", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ty_vendored = { git = "https://github.com/astral-sh/ruff.git", package = "ty_vendored", rev = "6ded4bed1651e30b34dd04cdaa50c763036abb0d" }
ruff_python_parser = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_parser", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" }
ruff_python_stdlib = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_stdlib", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" }
ruff_python_ast = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_python_ast", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" }
ruff_text_size = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_text_size", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" }
ruff_db = { git = "https://github.com/samuelcolvin/ruff.git", package = "ruff_db", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1", features = ["serde"] }
ty_python_semantic = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_python_semantic", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" }
ty_python_core = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_python_core", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" }
ty_module_resolver = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_module_resolver", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" }
ty_vendored = { git = "https://github.com/samuelcolvin/ruff.git", package = "ty_vendored", rev = "af72bbf75ff34f50f6fadf2ec3f1dec1af1eb5d1" }
# salsa version matches current main of ruff
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "53421c2fff87426fa0bb51cab06632b87646de13", default-features = false, features = [
salsa = { version="0.26.1", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
Expand Down
5 changes: 4 additions & 1 deletion crates/monty-python/python/pydantic_monty/_monty.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,10 @@ class MontySyntaxError(MontyError):
Inherits exception(), __str__() from MontyError.
"""

def display(self, format: Literal['type-msg', 'msg'] = 'msg') -> str:
def traceback(self) -> list[Frame]:
"""Returns the Monty traceback as a list of Frame objects."""

def display(self, format: Literal['traceback', 'type-msg', 'msg'] = 'traceback') -> str:
"""Returns formatted exception string.

Args:
Comment on lines +846 to 849
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 MontySyntaxError.display() pyi docstring incomplete for new 'traceback' format

The type stub at crates/monty-python/python/pydantic_monty/_monty.pyi:846 correctly lists 'traceback' in the Literal type and as the default, but the docstring at lines 849-851 only documents 'type-msg' and 'msg' formats — it doesn't document 'traceback'. Compare with MontyRuntimeError.display() at line 891-897 which documents all three formats. This is a documentation gap in the public API stub.

(Refers to lines 846-852)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 847 to 849
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MontySyntaxError.display() docstring missing documentation for 'traceback' format (now the default)

The .pyi docstring for MontySyntaxError.display() at crates/monty-python/python/pydantic_monty/_monty.pyi:846-852 does not document the 'traceback' format option, despite it being the new default. The docstring only lists 'type-msg' and 'msg'. Compare with MontyRuntimeError.display() at crates/monty-python/python/pydantic_monty/_monty.pyi:891-898, which correctly documents all three formats including 'traceback' - full traceback with exception. This violates CLAUDE.md: "If you encounter a comment or docstring that's out of date - you MUST update it to be correct."

(Refers to lines 847-852)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Expand Down
160 changes: 87 additions & 73 deletions crates/monty-python/src/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,62 +96,6 @@ impl MontyError {
}
}

/// Raised when Python code has syntax errors or cannot be parsed by Monty.
///
/// Inherits from `MontyError`. The inner exception is always a `SyntaxError`.
#[pyclass(extends=MontyError, module="pydantic_monty", skip_from_py_object)]
#[derive(Clone)]
pub struct MontySyntaxError;

impl MontySyntaxError {
/// Creates a new `MontySyntaxError` with the given message.
#[must_use]
pub fn new_err(py: Python<'_>, exc: MontyException) -> PyErr {
let base_error = MontyError::new(exc);
let init = PyClassInitializer::from(base_error).add_subclass(Self);
match Py::new(py, init) {
Ok(err) => PyErr::from_value(err.into_bound(py).into_any()),
Err(e) => e,
}
}
}

#[pymethods]
impl MontySyntaxError {
/// Returns formatted exception string.
///
/// Args:
/// format: 'type-msg' - 'ExceptionType: message' format
/// 'msg' - just the message
#[pyo3(signature = (format = "msg"))]
#[expect(clippy::needless_pass_by_value, reason = "required by macro")]
fn display(slf: PyRef<'_, Self>, format: &str) -> PyResult<String> {
let parent = slf.as_super();
match format {
"msg" => Ok(parent.message().unwrap_or_default().to_string()),
"type-msg" => Ok(parent.exc.summary()),
_ => Err(exceptions::PyValueError::new_err(format!(
"Invalid display format: '{format}'. Expected 'type-msg', or 'msg'"
))),
}
}

#[expect(clippy::needless_pass_by_value, reason = "required by macro")]
fn __str__(slf: PyRef<'_, Self>) -> String {
slf.as_super().message().unwrap_or_default().to_string()
}

#[expect(clippy::needless_pass_by_value, reason = "required by macro")]
fn __repr__(slf: PyRef<'_, Self>) -> String {
let parent = slf.as_super();
if let Some(msg) = parent.message() {
format!("MontySyntaxError({msg})")
} else {
"MontySyntaxError()".to_string()
}
}
}

/// Raised when type checking finds errors in the code.
///
/// Inherits from `MontyError`. This exception is raised when static type
Expand Down Expand Up @@ -202,37 +146,93 @@ impl MontyTypingError {
}
}

/// Raised when Python code has syntax errors or cannot be parsed by Monty.
///
/// Inherits from `MontyError`. The inner exception is always a `SyntaxError`.
#[pyclass(extends=MontyError, module="pydantic_monty", skip_from_py_object)]
pub struct MontySyntaxError {
traceback: Traceback,
}

impl MontySyntaxError {
/// Creates a new `MontySyntaxError` with the given message.
#[must_use]
pub fn new_err(py: Python<'_>, exc: MontyException) -> PyErr {
let traceback = match Traceback::new(py, &exc) {
Ok(frames) => frames,
Err(e) => return e,
};

let base_error = MontyError::new(exc);
let syntax_error = Self { traceback };
let init = PyClassInitializer::from(base_error).add_subclass(syntax_error);
match Py::new(py, init) {
Ok(err) => PyErr::from_value(err.into_bound(py).into_any()),
Err(e) => e,
}
}
}

#[pymethods]
impl MontySyntaxError {
/// Returns the Monty traceback as a list of Frame objects.
fn traceback(&self, py: Python<'_>) -> Py<PyList> {
self.traceback.py_list(py)
}

/// Returns formatted exception string.
#[pyo3(signature = (format = "traceback"))]
#[expect(clippy::needless_pass_by_value, reason = "required by macro")]
fn display(slf: PyRef<'_, Self>, format: &str) -> PyResult<String> {
match format {
"traceback" => Ok(slf.as_super().exc.to_string()),
"type-msg" => Ok(slf.as_super().exc.summary()),
"msg" => Ok(slf.as_super().message().unwrap_or_default().to_string()),
_ => Err(exceptions::PyValueError::new_err(format!(
"Invalid display format: '{format}'. Expected 'traceback', 'type-msg', or 'msg'"
))),
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}
}

#[expect(clippy::needless_pass_by_value, reason = "required by macro")]
fn __str__(slf: PyRef<'_, Self>) -> String {
slf.as_super().message().unwrap_or_default().to_string()
}

#[expect(clippy::needless_pass_by_value, reason = "required by macro")]
fn __repr__(slf: PyRef<'_, Self>) -> String {
let parent = slf.as_super();
if let Some(msg) = parent.message() {
format!("MontySyntaxError({msg})")
} else {
"MontySyntaxError()".to_string()
}
}
}

/// Raised when Monty code fails during execution.
///
/// Inherits from `MontyError`. Additionally provides `traceback()` to access
/// the Monty stack frames where the error occurred.
#[pyclass(extends=MontyError, module="pydantic_monty")]
pub struct MontyRuntimeError {
/// The traceback frames where the error occurred (pre-converted to Python objects).
frames: Vec<Py<PyFrame>>,
traceback: Traceback,
}

impl MontyRuntimeError {
/// Creates a new `MontyRuntimeError` from the given exception data.
#[must_use]
pub fn new_err(py: Python<'_>, exc: MontyException) -> PyErr {
// Convert stack frames to PyFrame objects
let frames_result: PyResult<Vec<Py<PyFrame>>> = exc
.traceback()
.iter()
.map(|f| Py::new(py, PyFrame::from_stack_frame(f)))
.collect();

let frames = match frames_result {
let traceback = match Traceback::new(py, &exc) {
Ok(frames) => frames,
Err(e) => return e,
};

let base_error = MontyError::new(exc);
// Create the MontyRuntimeError with proper initialization
let runtime_error = Self { frames };
let runtime_error = Self { traceback };

let init = pyo3::PyClassInitializer::from(base_error).add_subclass(runtime_error);
let init = PyClassInitializer::from(base_error).add_subclass(runtime_error);
match Py::new(py, init) {
Ok(err) => PyErr::from_value(err.into_bound(py).into_any()),
Err(e) => e,
Expand All @@ -244,14 +244,10 @@ impl MontyRuntimeError {
impl MontyRuntimeError {
/// Returns the Monty traceback as a list of Frame objects.
fn traceback(&self, py: Python<'_>) -> Py<PyList> {
PyList::new(py, &self.frames)
.expect("failed to create frames list")
.unbind()
self.traceback.py_list(py)
}

/// Returns formatted exception string.
///
/// Overrides the base class to provide the full traceback when format='traceback'.
#[pyo3(signature = (format = "traceback"))]
#[expect(clippy::needless_pass_by_value, reason = "required by macro")]
fn display(slf: PyRef<'_, Self>, format: &str) -> PyResult<String> {
Expand Down Expand Up @@ -290,6 +286,24 @@ impl MontyRuntimeError {
}
}

/// The traceback frames where the error occurred (pre-converted to Python objects).
struct Traceback(Vec<Py<PyFrame>>);

impl Traceback {
fn new(py: Python<'_>, exc: &MontyException) -> PyResult<Self> {
// Convert stack frames to PyFrame objects
exc.traceback()
.iter()
.map(|f| Py::new(py, PyFrame::from_stack_frame(f)))
.collect::<PyResult<Vec<Py<PyFrame>>>>()
.map(Self)
}

fn py_list(&self, py: Python<'_>) -> Py<PyList> {
PyList::new(py, &self.0).expect("failed to create frames list").unbind()
}
}

/// A single frame in a Monty traceback.
///
/// Contains all the information needed to display a traceback line:
Expand Down
13 changes: 10 additions & 3 deletions crates/monty-python/tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,15 @@ def test_str_returns_msg():
def test_syntax_error_display():
with pytest.raises(pydantic_monty.MontySyntaxError) as exc_info:
pydantic_monty.Monty('def')
assert exc_info.value.display() == snapshot('Expected an identifier at byte range 3..3')
assert exc_info.value.display('type-msg') == snapshot('SyntaxError: Expected an identifier at byte range 3..3')
assert exc_info.value.display() == snapshot("""\
Traceback (most recent call last):
File "main.py", line 1
def
~
SyntaxError: Expected an identifier\
""")
assert exc_info.value.display('type-msg') == snapshot('SyntaxError: Expected an identifier')
assert exc_info.value.display('msg') == snapshot('Expected an identifier')


def test_syntax_error_str():
Expand Down Expand Up @@ -419,7 +426,7 @@ def test_runtime_error_repr():
def test_syntax_error_repr():
with pytest.raises(pydantic_monty.MontySyntaxError) as exc_info:
pydantic_monty.Monty('def')
assert repr(exc_info.value) == snapshot('MontySyntaxError(Expected an identifier at byte range 3..3)')
assert repr(exc_info.value) == snapshot('MontySyntaxError(Expected an identifier)')


def test_frame_repr():
Expand Down
Loading
Loading