-
Notifications
You must be signed in to change notification settings - Fork 5
Replace RuntimeErrors with custom exceptions #486
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: main
Are you sure you want to change the base?
Conversation
Swap all RuntimeError exceptions for custom fab exceptions using a class hierarchy derived from RuntimeError to minimise distruptive changes.
Some minor flake8 problems had slipped in. This fixes them.
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.
Thanks for this huge amount of work. I didn't expect that many classes to be used :)
FWIW, in psyclone we have around half the errors defined in some submodules (which helps avoid circular dependencies to some degree).
I have added a few comments. Could you also add a section to the development.rst
file about errors, e.g. indicating that only Fab*
errors should be raised, no standard errors.
self.tool = tool | ||
|
||
# Check for name attributes rather than using isintance | ||
# because Category and Tool ahve issues with circular |
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.
I don't think Category has a problem by itself, so I think that can be moved out of the TYPE_CHECKING test above (and fix the type here from string to be Category) - at least it worked for me).
Then I find the name tool
confusing, since it can also be a category? Would it be worth storing both (tool and category, and leaving one of them None?? That might be useful for messages that a tool has the wrong category somewhere??). Or just call it tool_or_category
?
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.
I have thoughts on replacing the concept of categories with a more flexible system of capability tags. How does that interact with this issue?
# Check for name attributes rather than using isintance | ||
# because Category and Tool ahve issues with circular | ||
# dependencies | ||
if hasattr(tool, "name"): |
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.
With Category imported directly, you can use some isinstance
tests here. Otherwise I'd like a comment explaining what class you are accessing.
if hasattr(tool, "name"): | ||
self.name = tool.name | ||
elif hasattr(tool, "_name"): | ||
self.name = tool._name |
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.
Not sure what class this is, but please add a setter, don't access what is intended to be a private attribute.
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.
A getter in this case? Agree that we shouldn't be accessing nominally private state.
""" | ||
|
||
def __init__(self, tool: Union["Category", "Tool", str], message: str) -> None: | ||
self.tool = tool |
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.
Hmm - a coding style question: I prefer all attributes of a class to be 'private' (i.e. starting with _
, and then add getter and/or setter as required). The big advantage (imho) of this is that the setter/getter will be documented, so a user can check out the supported methods in the documentation.
We recently investigated the performance impact of a getter (see stfc/PSyclone#3082), and found a 3 to 4% improvement when avoiding the getter. That's for getters that are being called 100.000s of time, I don't think we will notice a performance disadvantage of this in Fab.
I am ok leaving it as is (esp. since afaik it's not a pep8 requirement), but would prefer a clarification in the coding style :)
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.
I too prefer private variables and getters/setters. It allows for validation on change of implementation without altering the API.
The @property
modifier is available if the ()
of a function offends.
preserved for inspection by the caller. | ||
""" | ||
|
||
def __init__(self, command: str) -> None: |
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.
Typing error: Union[list[str].str]
else: | ||
raise ValueError(f"invalid command: {command}") | ||
|
||
super().__init__(f"unable to execute {self.target}") |
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.
I would prefer quotes like '{self.target}'
around the string. This helps identify white-space errors (e.g. when a user provides say touch abc
as command, since we are running without shell, that line would result in unable to execute touch abc
, which kind of hides the error that there is a space in the binary name.
""" | ||
|
||
def __init__(self, tool: str, reason: str, revision: Optional[str] = None) -> None: | ||
self.tool = tool |
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.
I was confused at first, but then realised that this is not based on FabToolError. Nothing we can do about it, but maybe add a comment to explain this?
I'm about to start a review but I'll leave this comment here for immediate consideration. Are you aware of |
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.
This is an extensive change which tidies up exception handling considerable. A few questions and suggestions included in-line.
In general, we have generated API documentation but is there anything which could be added (or removed) from the static documentation in light of this change?
""" | ||
|
||
def __init__(self, tool: Union["Category", "Tool", str], message: str) -> None: | ||
self.tool = tool |
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.
I too prefer private variables and getters/setters. It allows for validation on change of implementation without altering the API.
The @property
modifier is available if the ()
of a function offends.
self.tool = tool | ||
|
||
# Check for name attributes rather than using isintance | ||
# because Category and Tool ahve issues with circular |
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.
I have thoughts on replacing the concept of categories with a more flexible system of capability tags. How does that interact with this issue?
self.tool = tool | ||
|
||
# Check for name attributes rather than using isintance | ||
# because Category and Tool ahve issues with circular |
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.
# because Category and Tool ahve issues with circular | |
# because Category and Tool have issues with circular |
if TYPE_CHECKING: | ||
from fab.tools.category import Category | ||
from fab.tools.tool import Tool |
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.
Don't like this. Is it due to the circular dependency you mention later?
if hasattr(tool, "name"): | ||
self.name = tool.name | ||
elif hasattr(tool, "_name"): | ||
self.name = tool._name |
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.
A getter in this case? Agree that we shouldn't be accessing nominally private state.
|
||
captured = capsys.readouterr() | ||
assert "error: fab file does not exist" in captured.err | ||
assert "fab file does not exist" in captured.err |
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.
Where we are testing error messages I think we should test as much as possible. i.e. prefer equality over "in".
# Mock defines an internal name property, so special handling | ||
# is required to reset the value to support testing | ||
tool = Mock(_name="tool cc") | ||
del tool.name | ||
err = FabToolError(tool, "compiler message") | ||
assert str(err) == "[tool cc] compiler message" |
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.
This seems alarming. Is this related to Joerg's comment about peeking at private state?
"""Test FabCommandError in various configurations.""" | ||
|
||
err = FabCommandError(["ls", "-l", "/nosuch"], 1, b"", b"ls: cannot", "/") | ||
assert str(err) == "command 'ls -l /nosuch' returned 1" |
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.
Would it be easier to read if the sentence was restructured to put the variable length element last. e.g. "Return code 1 from command: ls -l /nosuch".
This way the eye knows where to go to for the error code and the start of the command.
assert str(err) == "[git] merge failed: conflicting source files" | ||
|
||
err = FabSourceMergeError("git", "conflicting source files", "vn1.1") | ||
assert str(err) == "[git] merge of vn1.1 failed: conflicting source files" |
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.
Maybe quote marks around revision?
with raises(RuntimeError) as err: | ||
linker.get_lib_flags("unknown") | ||
assert str(err.value) == "Unknown library name: 'unknown'" | ||
assert str(err.value) == "unknown library unknown" |
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.
This might benefit from a redrafting. Initial upper-case and maybe quote marks around library name.
Swap all RuntimeError exceptions for custom fab exceptions using a class hierarchy derived from RuntimeError to minimise disruptive changes.
These changes fall into three broad areas:
fab.errors
and an associated set of testsRuntimeError
to test the impact on calling functions - match the expected instance of the new error classraise RuntimeError
across the code base with the new error classesWhile the change is complete and the test suite runs successfully in my environment, I still have a couple of outstanding questions that need to be addressed in review:
Error
but the derived classes do not. Ending every class name withError
would make fab more consistent with python, at the cost of longer exception names