Skip to content

Stricter ruff#180

Merged
rwb27 merged 38 commits into
mainfrom
stricter-ruff
Sep 11, 2025
Merged

Stricter ruff#180
rwb27 merged 38 commits into
mainfrom
stricter-ruff

Conversation

@rwb27
Copy link
Copy Markdown
Collaborator

@rwb27 rwb27 commented Aug 25, 2025

This PR adds a bunch of Ruff rulesets, notably the S rules (which enforce no assertions) and FAST rules (which are FastAPI specific, and disallow the use of Depends as a default value because it can cause confusing behaviour).

There are quite a few minor code changes as a result of this, mostly swapping assertions for explicit exceptions. As that changes test coverage, a few tests are added to explicitly test for the error conditions. Swapping assert for if...raise means that a test must be added to cover the raise line, or coverage will decrease.

I've also added ANN which enforces type annotations. This is mostly done by the stricter mypy rules, but it's nice having a Ruff check because it's much faster. Irritatingly, this complains about __init__ being untyped: I feel like __init__ does not need an annotation. However, I can't disable it for __init__ without disabling other magic functions as well, so I've added explicit -> None annotations throughout the codebase.

I added BLE to disallow except Exception: blocks, and spotted that one had crept in with a pull request: this is removed.

Changing asserts to exceptions also required a bunch of docstring updates, and proper mocking of Thing.path in some tests. I'd like to consolidate some of this code in a future PR.

@rwb27 rwb27 mentioned this pull request Aug 25, 2025
rwb27 added a commit that referenced this pull request Aug 25, 2025
I test for an error condition, and also test using the
endpoint functions not via HTTP.

It was necessary to add a default value for Thing.path to make
sure we raise the right error - this is needed by other checks
that are introduced in #180.
@rwb27 rwb27 force-pushed the stricter-ruff branch 2 times, most recently from 898b66a to 486c66a Compare August 25, 2025 18:31
@rwb27
Copy link
Copy Markdown
Collaborator Author

rwb27 commented Aug 25, 2025

I've paid a little "coverage tax" so the coverage is now increased. I'm starting to contemplate allowing slight (<0.1%) coverage decreases, as the coverage rule is now making it hard to delete code...

@julianstirling
Copy link
Copy Markdown
Contributor

I've paid a little "coverage tax" so the coverage is now increased. I'm starting to contemplate allowing slight (<0.1%) coverage decreases, as the coverage rule is now making it hard to delete code...

I think the check for decreased coverage is at times unhelpful because it can add to the temptation to just add tests that quickly touch any line of code rather than add clear specific tests. One thing we may want to consider is diff cover. This will check for missing coverage on new lines, so that it encourages specifically testing new code rather than adding any test to get the coverage up.

Copy link
Copy Markdown
Contributor

@julianstirling julianstirling left a comment

Choose a reason for hiding this comment

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

Really useful to have these extra rules.

A few small comments. I'd especially like to reserve no-cover for things that are truly not testable, and the one highlighted in the comments is easily testable

Comment thread src/labthings_fastapi/descriptors/endpoint.py
Comment thread src/labthings_fastapi/properties.py Outdated
Comment thread src/labthings_fastapi/thing_description/_model.py Outdated
@rwb27
Copy link
Copy Markdown
Collaborator Author

rwb27 commented Aug 27, 2025

I've paid a little "coverage tax" so the coverage is now increased. I'm starting to contemplate allowing slight (<0.1%) coverage decreases, as the coverage rule is now making it hard to delete code...

I think the check for decreased coverage is at times unhelpful because it can add to the temptation to just add tests that quickly touch any line of code rather than add clear specific tests. One thing we may want to consider is diff cover. This will check for missing coverage on new lines, so that it encourages specifically testing new code rather than adding any test to get the coverage up.

Something like diff-cover would be very helpful. I agree it's not always useful to have tests that increase coverage for the sake of it, rather than testing something useful. Most of the tests I've added have checked something (usually an error), but it feels slightly silly to be adding tests for code I've not touched.

I don't see a neat integration for diff-cover in GitHub, though I guess it doesn't need an integration if it's just passing or failing.

Added the NotConnectedToServerError exception.
I test for an error condition, and also test using the
endpoint functions not via HTTP.

It was necessary to add a default value for Thing.path to make
sure we raise the right error - this is needed by other checks
that are introduced in #180.
FastAPI allows dependencies to be specified either with an
annotated type, or by supplying Depends() as the default.

The latter form can cause problems, e.g. by masking the
fact that a dependency hasn't been injected. I've now
changed all occurrences of `Depends()` to use annotated
types instead, as required by the `FAST` ruleset in ruff.
Sphinx is configured with a Python script, but this is not part
of the package, the examples, or the test code. I've therefore
excluded it from the rules.

I've also changed the exclusion to be specific to the docs config file, and not the whole docs folder, just in case we
add scripts there in the future.

This also means any example Python files will be checked.
I've added module docstrings and some full stops, so the
docstrings in the example comply with the rules for the
module source.
Return type annotations for __init__ are not overly useful, and
are not required by `mypy`. However, other magic methods
do need an annotation, and it's helpful to have `ruff` check
this rather than waiting for `mypy`.

I've annotated all remaining `__init__` methods so this linter
rule now passes.

I also fixed a missing import from the change to Annotated
for FastAPI dependencies.
One assertion has simply been deleted (line 437) as it was not
clear how it could ever fail. An explicit type check a couple of
lines below ensures we will catch any errors here promptly,
so robustness is not affected.
I've exempted these from coverage: they are both primarily for
the benefit of mypy, as they only guard against weakrefs
failing. The objects referenced won't be deleted until after the
server shuts down, so these errors should never occur.
The action manager is added to a ThingServer during __init__
so it may never be None. The assertion is therefore not
necessary.
I've excluded this from coverage: it's testing against the blocking
portal being missing, in a dependency function. This dependency
function is not intended to be called by users, and it is
only evaluated by FastAPI while the server is running.
This was guarding a weakref to the Thing: this isn't an error we
expect, or an error we should handle, so I've excluded the
if block from testing. Coverage is unchanged: the assert
statement was equivalent to the new block, it just didn't
show as uncovered if the asertion passed.
This includes a couple of extra tests to ensure the right
error is raised if the functions are called prematurely.
Eval is flagged as potentially insecure. This function shouldn't
be run on user input, and already sanitises the string.

`blob_type` is also in the process of being phased out in favour
of subclassing Blob explicitly.
This will decrease coverage slightly.
Tests are needed to stop coverage decreasing, as otherwise
the exception isn't raised, and shows as uncovered.

These errors shouldn't occur in normal operation.
I've enabled a Ruff rule to disallow `except Exception:` and
removed one instance of that from the codebase.

I have left one `except Exception:` in test code (which captures
exceptions in a thread for later analysis), and another in the
MJPEG stream (which logs exceptions but does not crash the
server if they happen in the MJPEG stream).

The MJPEG code might be able to be eliminated now that
streams stop more gracefully - but I don't want to mess with
that until we're in a position to test it more extensively.
I've swapped the `logging.error` for `logging.exception` so
that the full stack trace is dumped to the log.
Test code that will fail without a path should mock one.
I've added a path in 3 places where this was required in
the test suite.
Exceptions raised when setting up things were being wrapped in an ExceptionGroup: this is fixed by using pytest.RasisesGroup, but that required updating pytest.

The test that checked for errors when a non-Thing was added to the server was failing, because the config was invalid.
I've fixed it, but we really ought to use a model here.

I've also renamed the import to avoid confusion with
`server`, so we now import the server module as `ts`.
This previously used an `assert` so that any types that weren't
models were handled in the `except` block.

I've changed this so that plain types are wrapped in a return
statement after the except: block, which I think is clearer
than the previous structure.
TestThing causes a warning in pytest: I've renamed to avoid
the confusion.
I was looking for the best place to add a test, and noticed the
comments/docstrings here needed improvement.
Previous tests didn't check what happened for bad action IDs
or actions with no output. This is now tested.
This adds a proper unit test for model_to_dict that checks all
code paths.
pydocstyle in flake8 gets confused by overloads. The same
rules in ruff behave correctly, and that's what the original
authors recommend.

I've removed pydocstyle from the dev dependencies already,
so this commit just removes the vestigial #noqa directives.
The set property endpoint function was returning `self.__set__`
which always returns `None`. The function is still called but,
for clarity, it is not returned. This is not a change in behaviour.

I've also improved the comment describing how we annotate
the `body` argument of the endpoint.
This checks that any URL specified for the "context" field of a
Thing Description is valid.
Error checking code added elsewhere now means Things
must have a `path` property or we can't generate the
Thing Description. I've added this manually in test code, so we
don't need a server.
Copy link
Copy Markdown
Contributor

@julianstirling julianstirling 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 the changes

@rwb27 rwb27 merged commit 5f510d8 into main Sep 11, 2025
14 checks passed
@rwb27 rwb27 deleted the stricter-ruff branch September 11, 2025 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants