Skip to content

Keep the base module providers on a subclass - #331

Merged
antonrh merged 1 commit into
mainfrom
fix-module-inheritance
Aug 31, 2026
Merged

antonrh merged 1 commit into
mainfrom
fix-module-inheritance

Conversation

@antonrh

@antonrh antonrh commented Aug 31, 2026 •

Copy link
Copy Markdown
Collaborator

Two bugs about inheritance, one in modules and one in the @singleton family.

A module subclass lost its base providers

ModuleMeta.__new__ collected providers from attrs, the attributes of 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)   # LookupError

The metaclass now starts from the bases and applies the class's own providers
on top. The collection is keyed by name, so a subclass that redefines a
provider replaces it instead of the module registering both, and an earlier
base wins over a later one, the way the MRO resolves an attribute.

An undecorated subclass counted as provided

is_provided read __provided__ with hasattr, and the attribute is
inherited, so every subclass of a provided class looked provided. The whole
metadata came with it, including the keys that belong to one class only.

An inherited alias registers the same interface twice:

@singleton(alias=Repo)
class SqlRepo: ...

class PostgresRepo(SqlRepo): ...    # nobody marked this one
ValueError: Alias `repos.Repo` is already registered for `repos.SqlRepo`.

An inherited from_context claims a registration nobody made:

LookupError: The provider `AuthedRequest` is registered with from_context=True
but has not been set in the request context.

And container.scan() registered every subclass it walked past, whether or
not anyone meant it to be a dependency.

is_provided now reads the class's own __dict__, the way
_check_already_provided already did. Decorating a subclass of a provided
class still works, and a test covers it.

This changes behaviour. A subclass that used to resolve by inheriting the
base's scope now raises LookupError naming the class and saying to decorate
it. Marking the class, or its own __provided__, is the fix. The pattern the
documentation shows, an undecorated base with the decorated class aliased to
it, is unaffected.

is_provider next to it stays on hasattr on purpose: providers are methods,
and a subclass inheriting one is exactly what the first fix needs.

@codspeed

codspeed Bot commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 17 untouched benchmarks


Comparing fix-module-inheritance (8bfe9fd) with main (f481f80)

Open in CodSpeed

A Module subclass rebuilt its provider list from its own attributes, so
inheriting a module dropped everything the base defined. A subclass that
redefines a provider now replaces it instead of registering both.

is_provided read the attribute through inheritance, so an undecorated
subclass of a provided class counted as provided and got scanned and
registered. It now reads the class's own __dict__.
@antonrh
antonrh force-pushed the fix-module-inheritance branch from 80c78a3 to 8bfe9fd Compare August 31, 2026 07:46
@antonrh
antonrh merged commit f351743 into main Aug 31, 2026
10 checks passed
@antonrh
antonrh deleted the fix-module-inheritance branch August 31, 2026 07:51
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