-
Notifications
You must be signed in to change notification settings - Fork 25
feat: rust error emitting #432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/oxidation
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| """Base error type supported by an Emitter.""" | ||
|
|
||
| class CraftError(BaseException): | ||
| """A program error to report with context.""" | ||
|
|
||
| message: str | ||
| """The main message to the user, to be shown as the first line of the error.""" | ||
|
|
||
| details: str | None | ||
| """Deeper, verbose error details that may help with diagnosing the deeper issue.""" | ||
|
|
||
| docs_url: str | None | ||
| """A URL to point the user towards for troubleshooting. | ||
|
|
||
| Supersedes `docs_slug`.""" | ||
|
|
||
| docs_slug: str | None | ||
| """A slug to append to user documentation. | ||
|
|
||
| This field is meant to be appended to a base URL provided by the method handling this error. | ||
|
|
||
| Is superseded by `docs_url`.""" | ||
|
|
||
| show_logpath: bool | ||
| """Whether to display the path to the log alongside this message. | ||
|
|
||
| It is generally recommended to leave this on, but some extremely simple error cases may | ||
| display better without the log path.""" | ||
|
|
||
| retcode: int | ||
| """The code to return when the application finishes. | ||
|
|
||
| This error class does not exit the program itself. Instead, this field is meant to be used | ||
| by a consumer inspecting the error message.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| message: str, | ||
| *, | ||
| details: str | None = None, | ||
| resolution: str | None = None, | ||
| docs_url: str | None = None, | ||
| docs_slug: str | None = None, | ||
| show_logpath: bool = True, | ||
| retcode: int = 1, | ||
| ) -> None: ... | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ use pyo3::{ | |
| }; | ||
|
|
||
| use crate::{ | ||
| errors::CraftError, | ||
| logs::LogListener, | ||
| printer::{Event, Target, Text}, | ||
| progress::Progresser, | ||
|
|
@@ -57,9 +58,6 @@ struct Emitter { | |
| /// The original filepath of the log file. | ||
| log_filepath: String, | ||
|
|
||
| // Used by `report_error` on the Python side, which was left in Python due to | ||
| // the retrieved errors all still being in Python. | ||
| #[expect(unused)] | ||
| /// The base URL for error messages. | ||
| docs_base_url: Option<String>, | ||
|
|
||
|
|
@@ -337,6 +335,59 @@ impl Emitter { | |
| Ok(()) | ||
| } | ||
|
|
||
| fn error(&mut self, error: &Bound<'_, CraftError>) -> PyResult<()> { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| self.report_error(error)?; | ||
| // Stop the emitter | ||
| self.finish() | ||
| } | ||
|
|
||
| fn report_error(&mut self, error: &Bound<'_, CraftError>) -> PyResult<()> { | ||
| if self.streaming_brief { | ||
| self.clear_prefix(); | ||
| } | ||
|
|
||
| // Build the message | ||
| let message = { | ||
| let error = error.borrow(); | ||
| let mut message = error.message.clone(); | ||
|
|
||
| if self.verbosity != Verbosity::Quiet | ||
| && let Some(ref details) = error.details | ||
| { | ||
| message.push_str(&format!("\n{details}")); | ||
| } | ||
|
|
||
| if let Some(ref resolution) = error.resolution { | ||
| message.push_str(&format!("\nRecommended resolution: {resolution}")); | ||
| } | ||
|
|
||
| // Give priority to `docs_url` in the event that it and `docs_slug` are both set. | ||
| if let Some(ref docs_url) = error.docs_url { | ||
| message.push_str(&format!("\nFor more information, visit {docs_url}")); | ||
| } else if let Some(ref docs_base_url) = self.docs_base_url | ||
| && let Some(ref doc_slug) = error.docs_slug | ||
| { | ||
| message.push_str(&format!( | ||
| "\nFor more information, visit {docs_base_url}{doc_slug}", | ||
| )); | ||
| } | ||
|
|
||
| if error.show_logpath { | ||
| message.push_str(&format!("\nFull execution log: {}", self.log_filepath)); | ||
| } | ||
|
|
||
| message | ||
| }; | ||
|
|
||
| let event = Event::Text(Text { | ||
| message, | ||
| target: Some(Target::Stderr), | ||
| permanent: true, | ||
| }); | ||
|
|
||
| crate::printer::printer().send(event) | ||
| } | ||
|
|
||
| /// Render an incremental progress bar. | ||
| #[pyo3(signature = ( | ||
| text, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| use pyo3::{ | ||
| Bound, PyResult, PyTypeInfo as _, | ||
| exceptions::PyBaseException, | ||
| pyclass, pymethods, pymodule, | ||
| types::{PyAnyMethods, PyDict, PyString, PySuper, PyTypeMethods}, | ||
| }; | ||
|
|
||
| #[derive(PartialEq)] | ||
| #[pyclass(extends = PyBaseException, subclass, get_all, set_all, eq)] | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This reads: "this class extends BaseException, is subclassable in Python, has getters & setters for each field, and is comparable with |
||
| pub struct CraftError { | ||
| pub message: String, | ||
| pub details: Option<String>, | ||
| pub resolution: Option<String>, | ||
| pub docs_url: Option<String>, | ||
| pub docs_slug: Option<String>, | ||
| pub show_logpath: bool, | ||
| pub retcode: i8, | ||
| } | ||
|
|
||
| #[pymethods] | ||
| impl CraftError { | ||
| #[new] | ||
| #[pyo3(signature = ( | ||
| message, | ||
| *, | ||
| details = None, | ||
| resolution = None, | ||
| docs_url = None, | ||
| docs_slug = None, | ||
| show_logpath = true, | ||
| retcode = 1 | ||
| ))] | ||
| fn new( | ||
| message: String, | ||
| details: Option<String>, | ||
| resolution: Option<String>, | ||
| docs_url: Option<String>, | ||
| docs_slug: Option<String>, | ||
| show_logpath: bool, | ||
| retcode: i8, | ||
| ) -> Self { | ||
| Self { | ||
| message, | ||
| details, | ||
| resolution, | ||
| docs_url, | ||
| docs_slug, | ||
| show_logpath, | ||
| retcode, | ||
| } | ||
| } | ||
|
|
||
| #[pyo3(name = "__init__", signature = (_message, **_kwargs))] | ||
| fn init( | ||
| slf: &Bound<'_, Self>, | ||
| _message: &Bound<'_, PyString>, | ||
| _kwargs: Option<&Bound<'_, PyDict>>, | ||
| ) -> PyResult<()> { | ||
| // See https://pyo3.rs/main/class.html#initializer for an explanation on why this method | ||
| // needs to exist. | ||
|
Comment on lines
+59
to
+60
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The TL;DR of this link is that because we extended a base Python type ( It seems like PyO3 was inserting the exact same arguments given to |
||
| // Call "super(self.__class__, self).__init__()" | ||
| PySuper::new(&PyBaseException::type_object(slf.py()), slf)?.call_method0("__init__")?; | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[pyo3(name = "__repr__")] | ||
| fn repr(slf: &Bound<'_, Self>) -> PyResult<String> { | ||
| Ok(format!( | ||
| "{}({:?})", | ||
| slf.get_type().qualname()?, | ||
| slf.borrow().message | ||
| )) | ||
| } | ||
|
|
||
| #[pyo3(name = "__str__")] | ||
| fn str(&self) -> String { | ||
| self.message.clone() | ||
| } | ||
| } | ||
|
|
||
| #[pymodule(submodule)] | ||
| pub mod errors { | ||
| use crate::utils::fix_imports; | ||
| use pyo3::types::PyModule; | ||
| use pyo3::{Bound, PyResult}; | ||
|
|
||
| #[pymodule_export] | ||
| use super::CraftError; | ||
|
|
||
| #[pymodule_init] | ||
| fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { | ||
| fix_imports(m, "craft_cli._rs.errors") | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -349,6 +349,9 @@ pub struct Printer { | |
|
|
||
| /// Whether or not a progress bar is currently running. | ||
| in_progress: bool, | ||
|
|
||
| /// Whether this printer has been stopped. | ||
| stopped: bool, | ||
| } | ||
|
|
||
| impl Printer { | ||
|
|
@@ -400,11 +403,18 @@ impl Printer { | |
| return Err(*e.downcast::<PyErr>().unwrap()); | ||
| } | ||
|
|
||
| self.stopped = true; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// Send a message to the `InnerPrinter` for displaying | ||
| pub fn send(&mut self, event: Event) -> PyResult<()> { | ||
| if self.stopped { | ||
| return Err(PyRuntimeError::new_err( | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @medubelko look for this pattern of "return Err(" when reviewing these PRs, any instance of this in Rust has the potential to be shown to the user if not caught (and many aren't caught right now, heh). But this is the only one in this PR anyways |
||
| "Attempted to print after the printer was stopped", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks fine, assuming there's a well-formed |
||
| )); | ||
| } | ||
| let prepared_event = self.prepare_event(event)?; | ||
| match prepared_event { | ||
| None => Ok(()), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are slightly reworked doc strings, and the
reportablefield was removed as the only time it was ever accessed in downstream code was to set it toFalse.