Skip to content

refactor(exceptions): split the module into a package by error family - #484

Merged
lesnik512 merged 1 commit into
mainfrom
split-exceptions-package
Sep 12, 2026
Merged

refactor(exceptions): split the module into a package by error family#484
lesnik512 merged 1 commit into
mainfrom
split-exceptions-package

Conversation

@lesnik512

@lesnik512 lesnik512 commented Sep 12, 2026

Copy link
Copy Markdown
Member

Closes #473.

modern_di/exceptions.py had grown to 759 lines across 33 classes, mixing container, resolution and registration errors with the warnings and the chain renderers. Each family now has its own submodule over a shared base and rendering.

module lines
lifecycle.py 44
warnings.py 58
rendering.py 64
base.py 66
__init__.py 88
container.py 154
resolution.py 182
registration.py 225

Backward compatibility

from modern_di import exceptions and from modern_di.exceptions import X are unchanged. The class bodies are a verbatim move: all 37 top-level blocks keep byte-identical ASTs, and the 34 public names are the same set.

The one user-visible consequence is __module__: ProviderNotRegisteredError.__module__ is now modern_di.exceptions.resolution, which shows up in tracebacks and pytest.raises output. Nothing in this repo asserts on it, and the filterwarnings string in docs/migration/to-3.x.md resolves through the re-export. A downstream snapshot test on traceback text would see a diff. Pinning __module__ back would hide the family from the traceback, so it is left honest.

Tests

No new tests, per the issue's "existing test coverage remains sufficient" — the suite is the same 540 passed at 100% line coverage as before the split, and the slug census is the structural gate it always was.

tests/test_error_rendering.py is the one test file that had to change: it reached for exceptions._render_chain and two sibling private helpers, which now live in rendering.

Known deviations from the issue

registration.py lands at 225 lines, over the under-200 in user story 3. Cutting it would either split the RegistrationError hierarchy across two files or force a family-to-family import; the family stays whole instead.

lifecycle.py is the weakest name in the set: it holds the three concretes that subclass ModernDIError directly (two close-time finalizer errors plus GroupInstantiationError). The alternative was folding GroupInstantiationError into registration.py, which reads naturally but puts a non-RegistrationError there.

Verification

ruff, ty, 540 passed, 100% line coverage.

@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: 000018c Previous: db82d66 Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 3986394.276875674 iter/sec (stddev: 9.226428653958042e-9) 6342740.37843326 iter/sec (stddev: 9.202513810841618e-9) 1.59
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 3969438.814235255 iter/sec (stddev: 1.0170923951089194e-8) 6301507.73655055 iter/sec (stddev: 5.837541794200572e-9) 1.59
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 15335.835653209153 iter/sec (stddev: 0.00019861890781159983) 18519.215140319986 iter/sec (stddev: 0.00023833044863198944) 1.21
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 14052.353791778007 iter/sec (stddev: 0.000046039640243974174) 16950.669911934823 iter/sec (stddev: 0.00003839170685752548) 1.21
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 564.0805935287412 iter/sec (stddev: 0.00005339801078745559) 750.5255911150985 iter/sec (stddev: 0.00010588905849466288) 1.33
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 488.6960676980392 iter/sec (stddev: 0.00018610505438532583) 716.130547342282 iter/sec (stddev: 0.000034490050698847745) 1.47
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 425.49793683877886 iter/sec (stddev: 0.00015066221618945393) 628.2716149468757 iter/sec (stddev: 0.000029724030420819064) 1.48
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 1692.5667461591281 iter/sec (stddev: 0.00020462147689898427) 2018.0238454413284 iter/sec (stddev: 0.0001466318768720067) 1.19
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 1374.1013162693375 iter/sec (stddev: 0.00017962626680038114) 1608.3153333810044 iter/sec (stddev: 0.00016296098788864177) 1.17
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 983.8653462210252 iter/sec (stddev: 0.00022490062966491276) 1140.4239451893473 iter/sec (stddev: 0.00016340204334424594) 1.16
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 712430.7354796415 iter/sec (stddev: 5.5084630197276516e-8) 900655.0058566307 iter/sec (stddev: 4.8654175268626705e-8) 1.26
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 686181.1908423759 iter/sec (stddev: 4.487490746408243e-8) 860547.5952628809 iter/sec (stddev: 3.913517920349686e-8) 1.25
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 2462.0281997604607 iter/sec (stddev: 0.00001260914087129064) 3131.2894785403664 iter/sec (stddev: 0.000014595809200219622) 1.27
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 61695.77495413011 iter/sec (stddev: 0.000002020824258095879) 88227.56125982522 iter/sec (stddev: 0.0000011915065519641762) 1.43
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 49561.605591185194 iter/sec (stddev: 0.0000018570590682547498) 59697.52510922353 iter/sec (stddev: 0.0000016626388028007914) 1.20
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 2533390.4022222282 iter/sec (stddev: 2.996034548731107e-8) 3340385.7226674776 iter/sec (stddev: 2.510624853208012e-8) 1.32
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 4210107.786759864 iter/sec (stddev: 9.292705959482407e-9) 6314036.165535865 iter/sec (stddev: 3.827483457802025e-9) 1.50
benchmarks/test_guard_resolve.py::test_g3_deep_chain 857197.129967429 iter/sec (stddev: 4.067180299596836e-8) 1133085.4463420268 iter/sec (stddev: 2.61991776066509e-7) 1.32
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 505915.83832159935 iter/sec (stddev: 4.137000268924078e-7) 639676.599981068 iter/sec (stddev: 2.962348952419589e-7) 1.26
benchmarks/test_guard_resolve.py::test_g5_cross_scope 1992410.6091549888 iter/sec (stddev: 2.780737749451298e-8) 2686527.267253543 iter/sec (stddev: 2.9619305062133404e-8) 1.35
benchmarks/test_guard_resolve.py::test_g9_context_resolve 1065353.0721271296 iter/sec (stddev: 1.515289661701316e-7) 1481506.2281223983 iter/sec (stddev: 1.4213959768046547e-7) 1.39
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 827460.8094854786 iter/sec (stddev: 3.647355959212343e-8) 1177838.2663165168 iter/sec (stddev: 4.5617617069912537e-8) 1.42
benchmarks/test_guard_resolve.py::test_g18_alias_hop 2800481.35791702 iter/sec (stddev: 1.7394036013799495e-8) 4079403.134366965 iter/sec (stddev: 2.1759382457417547e-8) 1.46
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 22776.810219049665 iter/sec (stddev: 0.00021489151358105248) 24499.653178795208 iter/sec (stddev: 0.00027135942114880316) 1.08
benchmarks/test_guard_validate.py::test_g11_validate_wide 15528.769027195749 iter/sec (stddev: 0.000023687384482807807) 18475.910332123764 iter/sec (stddev: 0.00001925423841397092) 1.19

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

Each family gets its own submodule over a shared `base` and `rendering`;
`__init__` re-exports every public name, so `from modern_di import exceptions`
and `from modern_di.exceptions import X` are unchanged.

The class bodies are a verbatim move: all 37 top-level blocks keep byte-identical
ASTs and the 34 public names are the same set. `__module__` now names the family
submodule, so it reads that way in tracebacks.

Closes #473
@lesnik512
lesnik512 force-pushed the split-exceptions-package branch from 6069929 to 000018c Compare September 12, 2026 11:13
@lesnik512
lesnik512 merged commit fc39046 into main Sep 12, 2026
10 checks passed
@lesnik512
lesnik512 deleted the split-exceptions-package branch September 12, 2026 11:19
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.

Split the exceptions module into a package

1 participant