Skip to content

Release 0.80.0 - #332

Merged
antonrh merged 1 commit into
mainfrom
prepare-0.80.0
Aug 31, 2026
Merged

antonrh merged 1 commit into
mainfrom
prepare-0.80.0

Conversation

@antonrh

@antonrh antonrh commented Aug 31, 2026 •

Copy link
Copy Markdown
Collaborator

Bumps the version to 0.80.0. A minor, because is_provided no longer counts an undecorated subclass as provided.

Merge this, then tag:

$ git tag 0.80.0 && git push origin 0.80.0

The workflow checks the tag against pyproject.toml, runs the linters and the tests, and publishes. Below is the text for the release notes.


Only a decorated class is provided

is_provided read __provided__ with hasattr, and a subclass inherits the
attribute. Every subclass of a provided class counted as provided, with the
base's whole metadata: container.scan() registered classes nobody marked, an
inherited alias registered the same interface twice, and an inherited
from_context claimed a registration nobody made.

Only the class a decorator was applied to is provided now:

@singleton
class BaseSender: ...

class SmtpSender(BaseSender): ...   # decorate this one as well

A subclass that used to resolve by inheriting its base's scope raises
LookupError naming the class and saying to decorate it.

A module subclass keeps its base providers

ModuleMeta collected providers from the class being defined, so a subclass
kept only what it declared itself:

class Base(Module):
    @provider(scope="singleton")
    def name(self) -> str:
        return "base"

class App(Base):
    @provider(scope="singleton")
    def port(self) -> int:
        return 8000

Container(modules=[App]).resolve(str)   # used to raise LookupError

A subclass that redefines a provider replaces it, and an earlier base wins over
a later one, the way the MRO resolves an attribute.

Reopening a container makes its resources again

Closing a container ran the finalisers but kept the instances, so the second
with handed out an object whose resource was already closed:

with container:
    conn = container.resolve(Connection)
with container:
    container.resolve(Connection)   # used to be the closed one

The instances whose finalisers ran are dropped on close. Everything else the
context holds stays, so an instance with no resource of its own is still the
same object across the two blocks.

Releasing an instance closes its resource

container.release(T) dropped the instance and left its generator open, so a
connection stayed checked out until the container closed. It now runs the
finaliser, and drops the instance even when the finaliser raises.
container.reset() releases everything before re-raising the first failure,
instead of stopping at it.

A scope whose entry fails unwinds

Python skips __exit__ when __enter__ raises, so a request scope that failed
part-way through entering left the resources it had already opened. It now
closes them before propagating.

Closing a context that holds async resources says so

Exiting a context with with when it holds an async resource left that
resource open and said nothing. It now raises:

RuntimeError: This context holds asynchronous resources. Close it with
`aclose()`, or exit it with `async with`, so that they are closed too.

Generic parameters a base class renamed

A class that renamed a TypeVar on its way down the hierarchy resolved to the
variable instead of the type:

class Repo(Generic[T]): ...
class Middle(Repo[U], Generic[U]): ...
class Users(Middle[User]): ...      # T is User, not U

Parameters nested inside an argument, like Repo[Box[T]], resolve too.

Framework extensions

  • A mounted application is walked by install() and reached at request
    time. Its routes were never wired, and a request into it raised
    AttributeError: 'State' object has no attribute 'container'.
  • A route added after install() is validated as it is added, with the
    message a route present at install time gets. It used to reach the handler
    unwired and raise TypeError: Dependency type is not set.
  • A FastStream subscriber with more than one handler has every handler
    injected, not just the first.
  • pydantic_settings.install() can run twice, with override=True, and
    names the model and the field when a provider is already registered.

Clearer failures

An extension whose package is missing says what to install:

ImportError: This extension needs `fastapi`. Install it with `pip install fastapi`.

The pytest plugin no longer breaks every test run when anyio renames a
helper, and reports what it could not import instead.

Dependencies

The wrapt floor moves to 1.17.3. container.ref() builds on the
pure-Python ObjectProxy, because the C one ignores the __wrapped__
override that makes a reference lazy, and wrapt.wrappers exposes the
pure-Python class from 1.17.3 on.

@codspeed

codspeed Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 17 untouched benchmarks


Comparing prepare-0.80.0 (f3eefe5) with main (f351743)

Open in CodSpeed

@antonrh
antonrh merged commit 2172c0c into main Aug 31, 2026
10 checks passed
@antonrh
antonrh deleted the prepare-0.80.0 branch August 31, 2026 07:59
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.

1 participant