Skip to content

Conversation

@t00sa
Copy link
Contributor

@t00sa t00sa commented Aug 22, 2025

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:

  • creation of a new set of error classes in fab.errors and an associated set of tests
  • changes to the existing regression tests to:
    • check the new message formats
    • check the exceptions - which are still caught as RuntimeError to test the impact on calling functions - match the expected instance of the new error class
  • replacement of raise RuntimeError across the code base with the new error classes

While 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:

  • are the names of the new exceptions classes acceptable? The top level class names end with Error but the derived classes do not. Ending every class name with Error would make fab more consistent with python, at the cost of longer exception names
  • are there any potential clashes or issues with the in-flight baf PR?

t00sa added 2 commits August 22, 2025 15:52
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.
@t00sa t00sa requested review from MatthewHambley and hiker August 22, 2025 15:35
@t00sa t00sa moved this to In Review in Fab Development Tracker Aug 22, 2025
@t00sa t00sa self-assigned this Aug 22, 2025
@t00sa t00sa linked an issue Aug 22, 2025 that may be closed by this pull request
Copy link
Collaborator

@hiker hiker left a 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
Copy link
Collaborator

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?

Copy link
Collaborator

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?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think my previous comment was a note to myself. It is an not an issue for this change.

if hasattr(tool, "name"):
self.name = tool.name
elif hasattr(tool, "_name"):
self.name = tool._name
Copy link
Collaborator

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.

Copy link
Collaborator

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
Copy link
Collaborator

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 :)

Copy link
Collaborator

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:
Copy link
Collaborator

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]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See Matthew's comment: we know that the command being passed to the constructor has a type of List[str], so we can change the type hinting and always execute the " ".join(command) line.

else:
raise ValueError(f"invalid command: {command}")

super().__init__(f"unable to execute {self.target}")
Copy link
Collaborator

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
Copy link
Collaborator

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?

@MatthewHambley
Copy link
Collaborator

I'm about to start a review but I'll leave this comment here for immediate consideration. Are you aware of FabException in https://github.com/MetOffice/fab/blob/main/source/fab/__init__.py? Have you considered how this interacts with your change?

Copy link
Collaborator

@MatthewHambley MatthewHambley left a 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
Copy link
Collaborator

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
Copy link
Collaborator

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?

if hasattr(tool, "name"):
self.name = tool.name
elif hasattr(tool, "_name"):
self.name = tool._name
Copy link
Collaborator

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.

Comment on lines 56 to 61
# 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"
Copy link
Collaborator

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?

@yaswant yaswant requested a review from hiker October 10, 2025 12:15
@hiker
Copy link
Collaborator

hiker commented Oct 10, 2025

@yaswant , I am a bit confused: I already did a review, and as far as I can see no work was done on the PR (and neither was there a reply to my comments). Additionally, the branch has conflicts ;)

@yaswant
Copy link
Collaborator

yaswant commented Oct 10, 2025

@hiker please ignore my assignment. You are right, Sam has not responded to your comments yet. Apologies for the confusion.

t00sa added 4 commits October 21, 2025 14:20
Replace the use of built-in python exceptions with Fab exception
classes and AssertionErrors.
There appear to be two problems with the github continuous integration
tests.

The format of the argparse help for choices is quoted in some
versions of python, so omit the choice strings from the output check.

The git system tests need details of the current user to run.  Add a
step which adds some temporary user details.
@t00sa t00sa requested a review from yaswant as a code owner October 22, 2025 14:22
Copy link
Contributor Author

@t00sa t00sa left a comment

Choose a reason for hiding this comment

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

Clearly, some things I still need to tidy up. I'll get on with them tomorrow and update the PR when they're ready.

Comment on lines +77 to +78
git config --global user.email "fab"
git config --global user.name "Fab Testing"
Copy link
Collaborator

Choose a reason for hiding this comment

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

I see you need this for git merge. There is no potential issue here, but does this have to be --global config? I know there is no strict validation here, but wondering if [email protected] as user.email would be better?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It probably doesn't have to be, but this is what git suggests when the configuration items don't exist

@t00sa
Copy link
Contributor Author

t00sa commented Oct 23, 2025

I have addressed the comments from the previous reviews, the CI tests pass, and I've successfully used my branch to build skeleton in LFRic core using Jason's branch, so I think this ready for another round of reviewing.

@t00sa t00sa requested a review from MatthewHambley October 23, 2025 10:25
@t00sa t00sa added the Ready for review Indicating that a PR is ready to be reviewed. label Oct 23, 2025
Copy link
Collaborator

@MatthewHambley MatthewHambley left a comment

Choose a reason for hiding this comment

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

While I think there's lots more work to do in the general area of error and exception handling, this is a good change and I'd rather see it on sooner and continue development than wait for it to be perfect.

A few comments left over from the previous review which haven't been addressed either with a change or a counter-comment.

In case you didn't know, if you from __future__ import annotate you don't need to quote type hints which have not yet been (or are currently being) defined.

if not isinstance(ar, Ar):
raise FabToolMismatch(ar.name, type(ar), "Ar")
output_fpath = str(output_fpath) if output_fpath else None
# output_fpath = str(output_fpath) if output_fpath else None
Copy link
Collaborator

Choose a reason for hiding this comment

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

Commented-out code - This looks like development detritus.

self.tool = tool

# Check for name attributes rather than using isintance
# because Category and Tool ahve issues with circular
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think my previous comment was a note to myself. It is an not an issue for this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Indicating that a PR is ready to be reviewed. technical debt

Projects

Status: In Review

Development

Successfully merging this pull request may close these issues.

Replace RuntimeError exceptions

4 participants