Skip to content

style: Fix raise-without-from-inside-except (B904) - #4032

Merged
wenzeslaus merged 18 commits into
OSGeo:mainfrom
echoix:fix-raise-without-from-inside-except
Jul 17, 2026
Merged

wenzeslaus merged 18 commits into
OSGeo:mainfrom
echoix:fix-raise-without-from-inside-except

Conversation

@echoix

@echoix echoix commented Jul 11, 2024 •

Copy link
Copy Markdown
Member

This PR I want a little bit more attentive review.

Ruff rule: https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/
The flake8 bugbear README also refers to the exception chaining tutorial here: https://docs.python.org/3/tutorial/errors.html#exception-chaining, quite useful to understand what exception chaining is (and is a very good thing for an application like ours when exceptions are rethrown to have more useful details for a user instead of scaring them off).

I wasn't completely sure to understand the existing patterns of exceptions, especially on how they were used.
For the ImportError one, there is no problem, it's obvious that it's good now.
Places where we wouldn't want exception chaining, and also not having the "During handling of the above exception, another exception occurred:" message when raising another exception in an except, we can use the "from None" idiom, and only the exception raised (inside the except) will be shown. Like in the python docs:

It also allows disabling automatic exception chaining using the from None idiom:

>>> try:
...    open('database.sqlite')
... except OSError:
...     raise RuntimeError from None

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError

https://docs.python.org/3/tutorial/errors.html#exception-chaining


Updated scope (by @wenzeslaus)

The branch has been merged up to current main (not rebased, to keep the shared
history intact). The non-style changes that were originally part of this PR were
split into separate PRs so they could be reviewed and merged on their own; all
four are now merged:

Those merged versions supersede the corresponding hunks that were here. This PR
enables the B904 rule and adds the from ... chaining across the rest of the
codebase. Two hunks in files the split PRs only partly touched remain here:
from error on extract_zip's BadZipfile in download.py, and from exc in
table.py execute().

Exceptions are chained with from error to preserve the original, including in
the places where the original is also passed into the message of the new
exception, so the detail appears both in the message and in the chained
traceback; reducing that duplication can be done later. from None is used where
the original would be noise: where it is an implementation detail of the raising
code (for example, IndexError becoming StopIteration) or where the new
exception gives a clearer replacement message (for example, an invalid value for
a database column). In wxGUI, the original error is passed into the new exception
and from None is used, because that exception is caught immediately and only its
message is shown, so no traceback is produced.

The three behavior fixes from the original PR are retained: the rdigit
ScriptError fix, the removed unreachable return in the toolbars, and the
removed dead try/except in __ll_parts (see the commit message).

Commit message

Suggested squash commit message:

style: Fix raise-without-from-inside-except (B904)

Enable the ruff B904 rule and add explicit exception chaining to raise
statements inside except clauses.

Most exceptions are chained with "from error" to preserve the original,
including where the original is also passed into the message of the new
exception, so the detail is both in the message and in the chained traceback;
reducing that duplication is left for later. "from None" is used where the
original would be noise: where it is an implementation detail of the raising
code (for example, IndexError becoming StopIteration) or where the new
exception gives a clearer replacement message (for example, an invalid value
for a database column). In wxGUI, the original error is passed into the new
exception and "from None" is used; that exception is caught immediately and
only its message is shown, so no traceback is produced.

The change covers wxGUI, grass.script, grass.pygrass, grass.gunittest,
grass.imaging, grass.pydispatch, grass.semantic_label, grass.utils, and
r.in.wms.

Related bug fixes included:

* rdigit: "raise ScriptError" raised the class, not an instance. ScriptError
  requires a message argument, so this raised TypeError instead, which the
  callers' "except ScriptError" handlers did not catch. A failing r.mapcalc
  or g.copy resulted in a traceback instead of showing an error message. The
  message of the module error is now passed to ScriptError.
* wxGUI toolbars: Remove an unreachable return statement after a raise.
* wxGUI utils: Remove a dead try-except in __ll_parts which could not be
  reached and only re-raised an empty ValueError.

@echoix echoix added this to the 8.5.0 milestone Jul 11, 2024
@echoix
echoix requested a review from ninsbl July 11, 2024 03:49
@echoix

echoix commented Jul 11, 2024

Copy link
Copy Markdown
Member Author

@ninsbl That's it for now, I don't have any more queued changes from the last week/weekends that are ready to be converted to a PR. I have one that needs to be applied after the pydispatch code synced from upstream, and another one that is maybe incomplete (it isn't done by ruff, it is manual from the actual pylint failures as ruff doesn't have that rule implemented yet, and since I never got to the point that the first pylint-related step in CI passes, I don't see if there's more.
My third set of changes are still a work-in-progress, as it's a bit a bigger change to make the (ruff) suggestion work in our context. (Since it removes some derived classe's init, I have to adjust the docstrings around and check that the sphinx-generated output is still fine. For now it isn't to my liking.

So, it'll take a couple days to have some more changes ready to review.

@github-actions github-actions Bot added GUI wxGUI related raster Related to raster data processing Python Related code is in Python libraries module labels Jul 11, 2024
@echoix
echoix force-pushed the fix-raise-without-from-inside-except branch from 9dd14c6 to d1f0e0e Compare July 13, 2024 12:06
@echoix
echoix requested review from ninsbl and removed request for ninsbl July 13, 2024 12:35
Comment thread gui/wxpython/tplot/frame.py Resolved
@echoix
echoix requested a review from ninsbl July 13, 2024 15:06

@ninsbl ninsbl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we touch upon some general questions here, where I would love to hear the opinion of e.g. @wenzeslaus or @petrasovaa ...

Comment thread python/grass/script/task.py Resolved
Comment thread python/grass/script/core.py Resolved
Comment thread python/grass/pygrass/rpc/base.py Resolved
@echoix

echoix commented Jul 13, 2024

Copy link
Copy Markdown
Member Author

If we are to address each of these for the best solution in context, it would make sense to expand the scope of the PR and use exception notes where it makes more sense.

@echoix
echoix requested a review from wenzeslaus August 1, 2024 16:49
@wenzeslaus

Copy link
Copy Markdown
Member

Raising exceptions versus printing error messages

Printing error messages is for command line tools. Python functions should generally raise exceptions.

The Python code in tools (modules; both in core and addons) should generally print an error message calling fatal. User will get the error in command line, GUI, or stderr in a subprocess call in Python. The fatal is a shortcut how to print an error message in a standardized way and end the execution of the process. A command line tool has a pretty good idea when to end the process.

Python functions in the library should raise exceptions to give the caller freedom to react to them in whatever way is appropriate. This is what is expected from a Python library as exceptions are the standard way in Python of reporting errors and a single function in a library does not have enough context to do anything more specific.

Tracebacks may be perceived by users as errors and are less readable then plain error messages, so it is desirable for the command line tool to catch exceptions and print error messages instead. This may also allow for switch from Python and function oriented wording to one more appropriate for a command line tool. However, not all possible exceptions need to be caught. Unlikely and unexpected exceptions unrelated to the standard execution of the program, e.g., programmer's errors, are best left uncaught so that full information in a debugging-friendly format, i.e., traceback, is displayed. The situation in the GUI is similar, although the dividing line is harder to spot and the details would be for a separate discussion.

This library-tool distinction gets complicated with how the library functions in the grass package were envisioned. Similarly to C, the idea for some of these functions was that they are solely meant for direct support of development of command line tools (I suppose; I was not here for that). Hence, it seemed convenient to prefer calls to fatal over raising exceptions. Given the benefits for writing tools, the practice spread widely at least over grass.script which would prevent its full use as a library, but at the same time, a function was introduced to control the behavior of fatal which now by default calls exit, but can also be set to raise an exception allowing its full use as a library. (We can consider for v9 whether the default should be changed.)

Comment thread python/grass/pygrass/vector/table.py Resolved
Comment thread python/grass/script/core.py Resolved
@echoix echoix added the conflicts/needs rebase Rebase to or merge with the latest base branch is needed label Dec 31, 2024
@echoix echoix removed the conflicts/needs rebase Rebase to or merge with the latest base branch is needed label Mar 30, 2025
echoix added 2 commits March 30, 2025 21:43
Rule contents: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
@wenzeslaus wenzeslaus modified the milestones: 8.5.0, 8.6.0 May 8, 2026
@wenzeslaus

Copy link
Copy Markdown
Member

I'm preparing an review-update here to finish things up, but it will depend on #7684.

@wenzeslaus

Copy link
Copy Markdown
Member

Now this depends on #7688, #7689 and #7690.

… clause

The raise is in the function body, not in an except clause, so B904 does
not apply to it. The 'from None' there would hide the original exception
if connection() is called while another exception is being handled.
This raise appeared on main after this branch was created and is the only
remaining B904 violation, so the rule cannot be enabled without it. The
KeyError is an implementation detail of the lookup, so it is suppressed.
The last fallback branch only indexes and assigns strings, so it cannot
raise ValueError and the handler was dead code re-raising an empty
ValueError.
…tract

The two adjacent handlers used 'err' and 'e' for the same kind of error.
The rest of the file and g.extension use 'error'.
…rom-inside-except

# Conflicts:
#	gui/wxpython/iscatt/plots.py
#	gui/wxpython/timeline/frame.py
#	gui/wxpython/tplot/frame.py
#	python/grass/gunittest/loader.py
#	python/grass/pygrass/vector/table.py
#	python/grass/utils/download.py
@wenzeslaus
wenzeslaus enabled auto-merge (squash) July 15, 2026 21:05
@wenzeslaus

Copy link
Copy Markdown
Member

There was a lot of related PRs to finalize this (in addition to the commits I made):

Split out of this one (merged):

Minimal Python version (the exception notes above need Python 3.11):

@echoix echoix left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@wenzeslaus I can't approve my own PR, so this is an approval of what's remaining

@wenzeslaus
wenzeslaus merged commit bcca890 into OSGeo:main Jul 17, 2026
24 of 27 checks passed
@echoix
echoix deleted the fix-raise-without-from-inside-except branch July 17, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

GUI wxGUI related libraries module Python Related code is in Python raster Related to raster data processing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants