Skip to content

feat: add provider.derive(fn) to inject a value computed from another provider - #733

Merged
lesnik512 merged 1 commit into
mainfrom
feat/provider-derive
Oct 19, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
feat/provider-derive

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #730.

Adds provider.derive(fn), a provider of fn(value), where value is what provider resolves to. It covers the settings case from discussion #136:

class Dependencies(Group):
    settings = providers.Factory(Settings, cache=True)
    database = providers.Factory(
        Database,
        kwargs={"dsn": settings.derive(lambda s: s.db_dsn)},
        cache=True,
    )

mypy, pyright and ty all infer s from settings and flag s.db_dns. A derived provider is never cached, has no scope of its own and is not registered under any type. Overriding the source reaches it, and so does overriding the derived provider directly. validate() checks the consuming factory against the source's effective scope and never calls fn. An exception from fn is re-raised unchanged with a note: raised by the function of Settings.derive (module:line).

Design notes:

  • The class _Derived is private and lives in providers/abstract.py, not providers/derived.py as the issue said. AbstractProvider.derive builds it, so a separate module would need a circular import.
  • dependency_graph.scope_chain extends terminal_chain through derived sources. effective_scope and the scope checks in collect_errors use it. redirect_target is unchanged, because a derived node is a step of its own in the chain, not a hop.
  • The resolver is a closure: fn(source_resolver(container)). That costs two Python calls per node, the same as a Factory, and test_derived_node_costs_its_resolver_frame_and_fn pins it.
  • AbstractProvider.__init__ takes call_depth so the call site recorded for a derived provider is the user's derive(...) line.
  • The note names only the derived step, not the whole chain from the root, the same way finalizer_note does.
  • There is no separate recipe page. The "Derived values" section in docs/providers/factories.md carries the settings example. The AttrGetter and .provided rows in the migration guides now point there.

#732, which gives inline providers the group's scope, is a separate branch. Whichever lands second needs a small rebase. #732's inline-scope walk will also need a branch for derived sources.

This was generated by AI.

… provider

settings.derive(lambda s: s.db_dsn) is a provider of fn(value). Type
checkers infer s from the source. It is never cached, has no scope of its
own and resolves at its source's effective scope, so it works inline in
kwargs in any group and an override of the source reaches it. An exception
raised by fn propagates unchanged, with a note naming the derived provider.

The class is private (_Derived) and lives in providers/abstract.py,
because the derive method on AbstractProvider needs it, and a separate
module would create an import cycle.

Closes #730
@lesnik512 lesnik512 added the enhancement New feature or request label Oct 19, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Benchmark

Details
Benchmark suite Current: 16b7f7e Previous: 5644031 Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 5688748.111334327 iter/sec (stddev: 1.835784278806345e-8) 4904470.719322522 iter/sec (stddev: 8.90402796485949e-9) 0.86
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 5859863.817923041 iter/sec (stddev: 1.6118092909924765e-8) 4840463.4085030025 iter/sec (stddev: 1.2665696484187963e-8) 0.83
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 19816.274017584732 iter/sec (stddev: 0.00004034859102604059) 23628.313618776076 iter/sec (stddev: 0.00003977866103226871) 1.19
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 15553.078006793326 iter/sec (stddev: 0.00013864966221817962) 18648.76958371226 iter/sec (stddev: 0.00013151010005662552) 1.20
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 777.851822658794 iter/sec (stddev: 0.000040883666344383664) 635.49521612325 iter/sec (stddev: 0.00010830725124873891) 0.82
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 732.7812404570436 iter/sec (stddev: 0.000056866581075568786) 622.2577111713392 iter/sec (stddev: 0.000022994150438437634) 0.85
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 688.3813254253176 iter/sec (stddev: 0.000055503270910848876) 600.8444213149718 iter/sec (stddev: 0.00003111836380343128) 0.87
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 16173.409136434311 iter/sec (stddev: 0.000011900032408579106) 19253.65454423782 iter/sec (stddev: 0.000006325155207578131) 1.19
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 6346.400837090051 iter/sec (stddev: 0.00003463308509007541) 9804.191504426777 iter/sec (stddev: 0.000018586115607355884) 1.54
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 3415.8624682695195 iter/sec (stddev: 0.00004535892669506383) 5039.582349677059 iter/sec (stddev: 0.00002463455424331871) 1.48
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] 12530.022455842092 iter/sec (stddev: 0.000010984717487878114) 14602.474998002726 iter/sec (stddev: 0.000027412516970430302) 1.17
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] 4786.674281731906 iter/sec (stddev: 0.00002930509145646027) 6404.044965525755 iter/sec (stddev: 0.000015517061243459845) 1.34
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] 2248.961668454162 iter/sec (stddev: 0.00005240594445521762) 3011.8917014085687 iter/sec (stddev: 0.000026324174080671828) 1.34
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[1] 31252.70205603029 iter/sec (stddev: 0.000006610706983284418) 53793.88067732621 iter/sec (stddev: 0.0000022875613335364985) 1.72
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[2] 11985.558600412329 iter/sec (stddev: 0.000024845369830575134) 23331.437654305966 iter/sec (stddev: 0.000006265710218518168) 1.95
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[4] 5438.430642952551 iter/sec (stddev: 0.00004095086582800911) 8590.035201249366 iter/sec (stddev: 0.000022493863850767943) 1.58
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 2250176.9201494516 iter/sec (stddev: 3.615768058648405e-8) 2113855.7590402174 iter/sec (stddev: 2.031861697710027e-8) 0.94
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 2461037.924291189 iter/sec (stddev: 3.886570985644965e-8) 2349543.3221972664 iter/sec (stddev: 1.6650926537950493e-8) 0.95
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 4180.551436665803 iter/sec (stddev: 0.000007008447618006861) 4451.999269637383 iter/sec (stddev: 0.000019825297086034126) 1.06
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 81831.96167710234 iter/sec (stddev: 0.0000011438983832709063) 75792.7049367867 iter/sec (stddev: 0.0000010181345487109347) 0.93
benchmarks/test_guard_lifecycle.py::test_g7b_request_cycle_sync 578864.4323811524 iter/sec (stddev: 6.561305324599335e-8) 579072.6614580763 iter/sec (stddev: 7.801953521917385e-8) 1.00
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 84016.68440863906 iter/sec (stddev: 0.0000013029683975836587) 89586.42894679612 iter/sec (stddev: 9.419924223584152e-7) 1.07
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers 767.6633772670398 iter/sec (stddev: 0.0012014510800948679) 808.4589055286162 iter/sec (stddev: 0.0010166585713580374) 1.05
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 3274826.376899151 iter/sec (stddev: 2.751959581092676e-8) 2993114.7137404513 iter/sec (stddev: 2.3521816194443065e-8) 0.91
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 5476376.281836439 iter/sec (stddev: 1.363982460503147e-8) 4816101.962687137 iter/sec (stddev: 5.295105482873398e-9) 0.88
benchmarks/test_guard_resolve.py::test_g3_deep_chain 1207090.2793101487 iter/sec (stddev: 4.1405091565502474e-8) 1113569.6873886315 iter/sec (stddev: 3.487097985959811e-8) 0.92
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 681966.4242686173 iter/sec (stddev: 3.612952682357208e-7) 612604.315410136 iter/sec (stddev: 3.4851084483792727e-7) 0.90
benchmarks/test_guard_resolve.py::test_g5_cross_scope 2825883.139694212 iter/sec (stddev: 2.3994413132611186e-8) 2674624.3874501064 iter/sec (stddev: 2.4053681698733978e-8) 0.95
benchmarks/test_guard_resolve.py::test_g9_context_resolve 2142767.452735959 iter/sec (stddev: 1.269114249373723e-7) 1960294.8165720731 iter/sec (stddev: 1.311245293630255e-7) 0.91
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 1223976.2754155502 iter/sec (stddev: 3.7142038160007894e-8) 1180518.9348746464 iter/sec (stddev: 5.260935842068453e-8) 0.96
benchmarks/test_guard_resolve.py::test_g18_alias_hop 5697638.516862223 iter/sec (stddev: 1.396202929205016e-8) 4813390.158237099 iter/sec (stddev: 4.760819374662499e-9) 0.84
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 42333.4150131789 iter/sec (stddev: 0.000015519171916960328) 53266.10819056237 iter/sec (stddev: 0.000014901713787393878) 1.26
benchmarks/test_guard_validate.py::test_g11_validate_wide 25905.449874576603 iter/sec (stddev: 0.00002038012677720931) 31125.67788213068 iter/sec (stddev: 0.000019062727395208582) 1.20

This comment was automatically generated by workflow using github-action-benchmark.

@lesnik512
lesnik512 merged commit 839b719 into main Oct 19, 2026
12 checks passed
@lesnik512
lesnik512 deleted the feat/provider-derive branch October 19, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add provider.derive(fn) to inject a value computed from another provider

1 participant