| Version 1 (modified by , 18 years ago) ( diff ) |
|---|
Error reporting guidelines for Boost.Build Python port
Goals
- For every error, we should be able to print the context of the error in user-visible terms, like "error when building target XXX". It should be necessary to associate error messages with Jamfiles, as much as possible -- for example, any error about building a target should say where that target is declared.
- For every error, likewise, we should be able to print the context of the error as stacktrace.
- The stacktrace should ideally show also Jamfiles from where Python is invoked.
- Python exceptions should never escape into Jam, as Jam cannot handle them in any way.
Concepts
We'll introduce 'user context', which is string description of what is being done now. The code will be pushing and popping user context as it does things. At any time, we'll have a list of currently active, nested, user context.
For accurate error reporting, all errors that are likely to be caused by user action should be reported via special function, that will record the current user context, so that it can be presented to user.
It's possible that we detect an error that is not directly traceable to user action, or Python interpreter itself detects an error and raise an Exception. We'll have try/except blocks at strategic places to associate such exceptions with user contexts.
The error handling documented here is
Reporting errors
Whenever we detect error that is traceable to user's mistake, we report it like this:
self.manager().errors()("something bad happened")
This call with create a raise an instance of ExceptionWithUserContext class, which keeps a record of what user-visible actions were performed at the point of error detection. This class also can print intself in a nice way.
When we detect an error that is totally unclear -- and could possibly be an internal error -- we just raise an exception.
Maintaining user context and handling stray exceptions
Any function that does something on interest to user must be written like this:
try:
self.manager().errors().push_user_context("Doing something")
do_something()
except ExceptionWithUserContext, e:
# We have exception with associated user context, just pass along
raise
except Exception, e:
# We have exception with no user context. Throw new exception
# that will be associated with the current user context, so that
# we have at least something to report.
self.mananger().errors().handle_stray_exception(e)
finally:
# In all cases, restore user context.
self.manager().errors().pop_user_context()
Handling Jam context
Most errors are result of some code in Jamfile, so it's important to show Jamfile lines in error
reports, too. To that effect, each function intended to be called from Jamfiles will call
Errors.push_jamfile_context and Errors.pop_jamfile_context. The first call will
include current jam stacktrace in the list of context, and that stacktrace will be printed
on errors. Note that this logic is completely implemented in project loading code, you
don't need to call those functions yourself, ever.
Capturing context
The targets are created when we parse Jamfiles, and built as a separate step. For all errors during building, it's desirable to report from where the target was declared. To that end:
- When the target is declared, we grab the current user context, calling
Errors.capture_user_context. - When actually building, the call to {{{Errors.
- For each error, we clearly will be able to produce
a raw stacktrace, but it's not very useful. Therefore, we need some 'user-context' mode of error reporting, where user is told what high-level actions were performed when the error happened. For example:
error: invalid feature 'non-existent'
- when computing build properties for target 'foo'
- when build project in '.'
To implement this we need:
- Maintain those user-contexts
- Arrange for user-context to be printed or recorded when error happens
- Do something about 'stray' exceptions through in Python code, like caused by bugs
- We should have a module errors with two functions:
'push-context' and 'pop-context'. The first takes a string description of what we're doing now. Every function that does something interesting from the user's point of view should be written like:
try:
errors.push_context("Doing something") # do something
finally:
errors.pop_context()
The try/finally is needed so that if stray exception is thrown, the pushed used context does not stay pushed forever.
- Whenever we run into error condition that can be explained in
user terms, we'll use errors.error(). That function will create new exception type, record current user context into it, and raise that exception.
- Python exception should never be allowed to escape back to jam.
Every function called from bjam should try/catch and print the exception.
- Every function called from bjam will get bjam stack trace
and add it to user context. So, if error happens user will see what line from Jamfile caused it.
- It's possible that we either detect error at too low
level to provide any explanation, or the error is detected by Python interpreter. In which case, an ordinary exception will be thrown. To still provide some context, we should use try/catch at various strategic places, catch such stray exceptions, and throw new exception, that holds current user context and the original exception. So, if we try to say interate over None somewhere deep, we'll get an error message like this:
internal-error: Itetarion over non-sequence
- when building target 'foo'
Use --backtrace to see Python backtrace
Note that this output is not very useful to debug anything. It's primarily meant to avoid completely freaking out users who are not accustomed to Python stacktraces. This way, they'll at least have the time to open Python docs before getting real Python stracktrace.
- When printing real stacktrace, we can be inside Python code
called from Jam code, called from Python code etc. Technically, it appears possible to interleave stacktraces, something like:
build_system.py:100 Jamroot:12 project.py:70
