From 039add2f137c80a02e42ee8e5541113708cdde0b Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Wed, 9 Apr 2025 05:50:33 +0200 Subject: [PATCH 01/13] Start the documentation for rodi --- .gitignore | 1 - rodi/Makefile | 24 ++ rodi/README.md | 3 + rodi/docs/about.md | 9 + rodi/docs/css/extra.css | 123 +++++++ rodi/docs/css/neoteroi.css | 1 + rodi/docs/getting-started.md | 492 ++++++++++++++++++++++++++ rodi/docs/img/neoteroi-w.svg | 74 ++++ rodi/docs/img/neoteroi.ico | Bin 0 -> 305984 bytes rodi/docs/index.md | 29 ++ rodi/docs/js/fullscreen.js | 26 ++ rodi/mkdocs.yml | 76 ++++ rodi/overrides/main.html | 43 +++ rodi/overrides/partials/comments.html | 49 +++ rodi/overrides/partials/content.html | 16 + rodi/overrides/partials/header.html | 76 ++++ 16 files changed, 1041 insertions(+), 1 deletion(-) create mode 100644 rodi/Makefile create mode 100644 rodi/README.md create mode 100644 rodi/docs/about.md create mode 100644 rodi/docs/css/extra.css create mode 100644 rodi/docs/css/neoteroi.css create mode 100644 rodi/docs/getting-started.md create mode 100644 rodi/docs/img/neoteroi-w.svg create mode 100644 rodi/docs/img/neoteroi.ico create mode 100644 rodi/docs/index.md create mode 100644 rodi/docs/js/fullscreen.js create mode 100644 rodi/mkdocs.yml create mode 100644 rodi/overrides/main.html create mode 100644 rodi/overrides/partials/comments.html create mode 100644 rodi/overrides/partials/content.html create mode 100644 rodi/overrides/partials/header.html diff --git a/.gitignore b/.gitignore index 1356e3a..8669775 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,5 @@ out .local # temporary -rodi shared-assets copy-shared.sh diff --git a/rodi/Makefile b/rodi/Makefile new file mode 100644 index 0000000..467ec68 --- /dev/null +++ b/rodi/Makefile @@ -0,0 +1,24 @@ +.PHONY: build fixlinks +include .env + +build: + mkdocs build + ./fixlinks.sh + rm -rf .build + mkdir -p .build/blacksheep + mv site/* .build/blacksheep + echo "Ready to publish" + + +build-v1: + mkdocs build + VERSION="v1" ./fixlinks.sh + rm -rf .build + mkdir -p .build/blacksheep/v1 + mv site/* .build/blacksheep/v1 + echo "Ready to publish" + + +clean: + rm -rf site/ + rm -rf .build/ diff --git a/rodi/README.md b/rodi/README.md new file mode 100644 index 0000000..1ca1b43 --- /dev/null +++ b/rodi/README.md @@ -0,0 +1,3 @@ +# Rodi docs 📜 + +[www.neoteroi.dev](https://www.neoteroi.dev/rodi/). diff --git a/rodi/docs/about.md b/rodi/docs/about.md new file mode 100644 index 0000000..d69dca1 --- /dev/null +++ b/rodi/docs/about.md @@ -0,0 +1,9 @@ +# About Rodi + +... + +## The project's home + +The project is hosted in [GitHub](https://github.com/Neoteroi/rodi), +handled following DevOps good practices, features 100% code coverage, and is +published to [pypi.org](https://pypi.org/project/rodi/). diff --git a/rodi/docs/css/extra.css b/rodi/docs/css/extra.css new file mode 100644 index 0000000..468ae82 --- /dev/null +++ b/rodi/docs/css/extra.css @@ -0,0 +1,123 @@ +[data-md-color-scheme=slate] { + --md-code-hl-comment-color: #33b227 !important; /* #b28027 */ +} + +[data-md-color-scheme=default] { + --md-code-hl-comment-color: #ab0404; +} + +html { + overflow-y: scroll; +} + +@media screen and (min-width: 1000px) { + html.fullscreen { + .md-grid { + max-width: 98%; + } + + .md-sidebar { + width: auto; + min-width: 15%; + } + } +} + +#fullscreen-form label { + display: none; +} + +html:not(.fullscreen) #full-screen { + display: inline-block !important; +} + +html.fullscreen #full-screen-exit { + display: inline-block !important; +} + +html.fullscreen #full-screen { + display: none !important; +} + +[data-md-color-scheme="default"] .md-typeset [type="checkbox"]:checked + .task-list-indicator::before { + background-color: #2d319f; +} + +[data-md-color-scheme="slate"] .md-typeset [type="checkbox"]:checked + .task-list-indicator::before { + background-color: #00e6e6; +} + +[data-md-color-scheme="slate"] { + --md-accent-fg-color: #21bbd0; + --md-primary-fg-color: #53ebff; + --md-primary-fg-color--light: #5db0c0; + --md-primary-fg-color--dark: #308ea1; +} + +.md-search__input, .md-header .md-search__input::placeholder, .md-search__input + .md-search__icon { + color: #000 !important; +} + +.md-header { + background-color: var(--bg-color, teal); +} + +.md-content article { + margin-bottom: 3em; +} + +.md-header .md-search__input { + background-color: #fff; +} + +.md-header-nav__button.md-logo img, .md-header-nav__button.md-logo svg { + width: 1.8rem; + height: auto; +} + +.img-full-width + p img { + width: 100%; +} + +.img-auto-width + p img { + width: none; +} + +.md-typeset h1 { + margin: 0 0 1em; +} + +.small { + font-size: 14px; +} + +span.task-list-indicator { + margin-right: 5px; +} + +@media screen and (max-width: 76.1875em) { + .md-nav--primary .md-nav__title[for=__drawer] { + background-color: #000000; + } +} + +:root { + --nt-color-7: #108d10; +} + +.version-warning { + margin-top: 5px !important; +} + +.md-typeset .tabbed-labels>label, .md-typeset .admonition, .md-typeset details { + font-size: .75rem !important; +} +/* +.md-typeset code { + font-size: .9em !important; +} + +table td code { + white-space: nowrap; +} +*/ diff --git a/rodi/docs/css/neoteroi.css b/rodi/docs/css/neoteroi.css new file mode 100644 index 0000000..cd8c6cf --- /dev/null +++ b/rodi/docs/css/neoteroi.css @@ -0,0 +1 @@ +:root{--nt-color-0: #CD853F;--nt-color-1: #B22222;--nt-color-2: #000080;--nt-color-3: #4B0082;--nt-color-4: #3CB371;--nt-color-5: #D2B48C;--nt-color-6: #FF00FF;--nt-color-7: #98FB98;--nt-color-8: #FFEBCD;--nt-color-9: #2E8B57;--nt-color-10: #6A5ACD;--nt-color-11: #48D1CC;--nt-color-12: #FFA500;--nt-color-13: #F4A460;--nt-color-14: #A52A2A;--nt-color-15: #FFE4C4;--nt-color-16: #FF4500;--nt-color-17: #AFEEEE;--nt-color-18: #FA8072;--nt-color-19: #2F4F4F;--nt-color-20: #FFDAB9;--nt-color-21: #BC8F8F;--nt-color-22: #FFC0CB;--nt-color-23: #00FA9A;--nt-color-24: #F0FFF0;--nt-color-25: #FFFACD;--nt-color-26: #F5F5F5;--nt-color-27: #FF6347;--nt-color-28: #FFFFF0;--nt-color-29: #7FFFD4;--nt-color-30: #E9967A;--nt-color-31: #7B68EE;--nt-color-32: #FFF8DC;--nt-color-33: #0000CD;--nt-color-34: #D2691E;--nt-color-35: #708090;--nt-color-36: #5F9EA0;--nt-color-37: #008080;--nt-color-38: #008000;--nt-color-39: #FFE4E1;--nt-color-40: #FFFF00;--nt-color-41: #FFFAF0;--nt-color-42: #DCDCDC;--nt-color-43: #ADFF2F;--nt-color-44: #ADD8E6;--nt-color-45: #8B008B;--nt-color-46: #7FFF00;--nt-color-47: #800000;--nt-color-48: #20B2AA;--nt-color-49: #556B2F;--nt-color-50: #778899;--nt-color-51: #E6E6FA;--nt-color-52: #FFFAFA;--nt-color-53: #FF7F50;--nt-color-54: #FF0000;--nt-color-55: #F5DEB3;--nt-color-56: #008B8B;--nt-color-57: #66CDAA;--nt-color-58: #808000;--nt-color-59: #FAF0E6;--nt-color-60: #00BFFF;--nt-color-61: #C71585;--nt-color-62: #00FFFF;--nt-color-63: #8B4513;--nt-color-64: #F0F8FF;--nt-color-65: #FAEBD7;--nt-color-66: #8B0000;--nt-color-67: #4682B4;--nt-color-68: #F0E68C;--nt-color-69: #BDB76B;--nt-color-70: #A0522D;--nt-color-71: #FAFAD2;--nt-color-72: #FFD700;--nt-color-73: #DEB887;--nt-color-74: #E0FFFF;--nt-color-75: #8A2BE2;--nt-color-76: #32CD32;--nt-color-77: #87CEFA;--nt-color-78: #00CED1;--nt-color-79: #696969;--nt-color-80: #DDA0DD;--nt-color-81: #EE82EE;--nt-color-82: #FFB6C1;--nt-color-83: #8FBC8F;--nt-color-84: #D8BFD8;--nt-color-85: #9400D3;--nt-color-86: #A9A9A9;--nt-color-87: #FFFFE0;--nt-color-88: #FFF5EE;--nt-color-89: #FFF0F5;--nt-color-90: #FFDEAD;--nt-color-91: #800080;--nt-color-92: #B0E0E6;--nt-color-93: #9932CC;--nt-color-94: #DAA520;--nt-color-95: #F0FFFF;--nt-color-96: #40E0D0;--nt-color-97: #00FF7F;--nt-color-98: #006400;--nt-color-99: #808080;--nt-color-100: #87CEEB;--nt-color-101: #0000FF;--nt-color-102: #6495ED;--nt-color-103: #FDF5E6;--nt-color-104: #B8860B;--nt-color-105: #BA55D3;--nt-color-106: #C0C0C0;--nt-color-107: #000000;--nt-color-108: #F08080;--nt-color-109: #B0C4DE;--nt-color-110: #00008B;--nt-color-111: #6B8E23;--nt-color-112: #FFE4B5;--nt-color-113: #FFA07A;--nt-color-114: #9ACD32;--nt-color-115: #FFFFFF;--nt-color-116: #F5F5DC;--nt-color-117: #90EE90;--nt-color-118: #1E90FF;--nt-color-119: #7CFC00;--nt-color-120: #FF69B4;--nt-color-121: #F8F8FF;--nt-color-122: #F5FFFA;--nt-color-123: #00FF00;--nt-color-124: #D3D3D3;--nt-color-125: #DB7093;--nt-color-126: #DA70D6;--nt-color-127: #FF1493;--nt-color-128: #228B22;--nt-color-129: #FFEFD5;--nt-color-130: #4169E1;--nt-color-131: #191970;--nt-color-132: #9370DB;--nt-color-133: #483D8B;--nt-color-134: #FF8C00;--nt-color-135: #EEE8AA;--nt-color-136: #CD5C5C;--nt-color-137: #DC143C}:root{--nt-group-0-main: #000000;--nt-group-0-dark: #FFFFFF;--nt-group-0-light: #000000;--nt-group-0-main-bg: #F44336;--nt-group-0-dark-bg: #BA000D;--nt-group-0-light-bg: #FF7961;--nt-group-1-main: #000000;--nt-group-1-dark: #FFFFFF;--nt-group-1-light: #000000;--nt-group-1-main-bg: #E91E63;--nt-group-1-dark-bg: #B0003A;--nt-group-1-light-bg: #FF6090;--nt-group-2-main: #FFFFFF;--nt-group-2-dark: #FFFFFF;--nt-group-2-light: #000000;--nt-group-2-main-bg: #9C27B0;--nt-group-2-dark-bg: #6A0080;--nt-group-2-light-bg: #D05CE3;--nt-group-3-main: #FFFFFF;--nt-group-3-dark: #FFFFFF;--nt-group-3-light: #000000;--nt-group-3-main-bg: #673AB7;--nt-group-3-dark-bg: #320B86;--nt-group-3-light-bg: #9A67EA;--nt-group-4-main: #FFFFFF;--nt-group-4-dark: #FFFFFF;--nt-group-4-light: #000000;--nt-group-4-main-bg: #3F51B5;--nt-group-4-dark-bg: #002984;--nt-group-4-light-bg: #757DE8;--nt-group-5-main: #000000;--nt-group-5-dark: #FFFFFF;--nt-group-5-light: #000000;--nt-group-5-main-bg: #2196F3;--nt-group-5-dark-bg: #0069C0;--nt-group-5-light-bg: #6EC6FF;--nt-group-6-main: #000000;--nt-group-6-dark: #FFFFFF;--nt-group-6-light: #000000;--nt-group-6-main-bg: #03A9F4;--nt-group-6-dark-bg: #007AC1;--nt-group-6-light-bg: #67DAFF;--nt-group-7-main: #000000;--nt-group-7-dark: #000000;--nt-group-7-light: #000000;--nt-group-7-main-bg: #00BCD4;--nt-group-7-dark-bg: #008BA3;--nt-group-7-light-bg: #62EFFF;--nt-group-8-main: #000000;--nt-group-8-dark: #FFFFFF;--nt-group-8-light: #000000;--nt-group-8-main-bg: #009688;--nt-group-8-dark-bg: #00675B;--nt-group-8-light-bg: #52C7B8;--nt-group-9-main: #000000;--nt-group-9-dark: #FFFFFF;--nt-group-9-light: #000000;--nt-group-9-main-bg: #4CAF50;--nt-group-9-dark-bg: #087F23;--nt-group-9-light-bg: #80E27E;--nt-group-10-main: #000000;--nt-group-10-dark: #000000;--nt-group-10-light: #000000;--nt-group-10-main-bg: #8BC34A;--nt-group-10-dark-bg: #5A9216;--nt-group-10-light-bg: #BEF67A;--nt-group-11-main: #000000;--nt-group-11-dark: #000000;--nt-group-11-light: #000000;--nt-group-11-main-bg: #CDDC39;--nt-group-11-dark-bg: #99AA00;--nt-group-11-light-bg: #FFFF6E;--nt-group-12-main: #000000;--nt-group-12-dark: #000000;--nt-group-12-light: #000000;--nt-group-12-main-bg: #FFEB3B;--nt-group-12-dark-bg: #C8B900;--nt-group-12-light-bg: #FFFF72;--nt-group-13-main: #000000;--nt-group-13-dark: #000000;--nt-group-13-light: #000000;--nt-group-13-main-bg: #FFC107;--nt-group-13-dark-bg: #C79100;--nt-group-13-light-bg: #FFF350;--nt-group-14-main: #000000;--nt-group-14-dark: #000000;--nt-group-14-light: #000000;--nt-group-14-main-bg: #FF9800;--nt-group-14-dark-bg: #C66900;--nt-group-14-light-bg: #FFC947;--nt-group-15-main: #000000;--nt-group-15-dark: #FFFFFF;--nt-group-15-light: #000000;--nt-group-15-main-bg: #FF5722;--nt-group-15-dark-bg: #C41C00;--nt-group-15-light-bg: #FF8A50;--nt-group-16-main: #FFFFFF;--nt-group-16-dark: #FFFFFF;--nt-group-16-light: #000000;--nt-group-16-main-bg: #795548;--nt-group-16-dark-bg: #4B2C20;--nt-group-16-light-bg: #A98274;--nt-group-17-main: #000000;--nt-group-17-dark: #FFFFFF;--nt-group-17-light: #000000;--nt-group-17-main-bg: #9E9E9E;--nt-group-17-dark-bg: #707070;--nt-group-17-light-bg: #CFCFCF;--nt-group-18-main: #000000;--nt-group-18-dark: #FFFFFF;--nt-group-18-light: #000000;--nt-group-18-main-bg: #607D8B;--nt-group-18-dark-bg: #34515E;--nt-group-18-light-bg: #8EACBB}.nt-pastello{--nt-group-0-main: #000000;--nt-group-0-dark: #000000;--nt-group-0-light: #000000;--nt-group-0-main-bg: #EF9A9A;--nt-group-0-dark-bg: #BA6B6C;--nt-group-0-light-bg: #FFCCCB;--nt-group-1-main: #000000;--nt-group-1-dark: #000000;--nt-group-1-light: #000000;--nt-group-1-main-bg: #F48FB1;--nt-group-1-dark-bg: #BF5F82;--nt-group-1-light-bg: #FFC1E3;--nt-group-2-main: #000000;--nt-group-2-dark: #000000;--nt-group-2-light: #000000;--nt-group-2-main-bg: #CE93D8;--nt-group-2-dark-bg: #9C64A6;--nt-group-2-light-bg: #FFC4FF;--nt-group-3-main: #000000;--nt-group-3-dark: #000000;--nt-group-3-light: #000000;--nt-group-3-main-bg: #B39DDB;--nt-group-3-dark-bg: #836FA9;--nt-group-3-light-bg: #E6CEFF;--nt-group-4-main: #000000;--nt-group-4-dark: #000000;--nt-group-4-light: #000000;--nt-group-4-main-bg: #9FA8DA;--nt-group-4-dark-bg: #6F79A8;--nt-group-4-light-bg: #D1D9FF;--nt-group-5-main: #000000;--nt-group-5-dark: #000000;--nt-group-5-light: #000000;--nt-group-5-main-bg: #90CAF9;--nt-group-5-dark-bg: #5D99C6;--nt-group-5-light-bg: #C3FDFF;--nt-group-6-main: #000000;--nt-group-6-dark: #000000;--nt-group-6-light: #000000;--nt-group-6-main-bg: #81D4FA;--nt-group-6-dark-bg: #4BA3C7;--nt-group-6-light-bg: #B6FFFF;--nt-group-7-main: #000000;--nt-group-7-dark: #000000;--nt-group-7-light: #000000;--nt-group-7-main-bg: #80DEEA;--nt-group-7-dark-bg: #4BACB8;--nt-group-7-light-bg: #B4FFFF;--nt-group-8-main: #000000;--nt-group-8-dark: #000000;--nt-group-8-light: #000000;--nt-group-8-main-bg: #80CBC4;--nt-group-8-dark-bg: #4F9A94;--nt-group-8-light-bg: #B2FEF7;--nt-group-9-main: #000000;--nt-group-9-dark: #000000;--nt-group-9-light: #000000;--nt-group-9-main-bg: #A5D6A7;--nt-group-9-dark-bg: #75A478;--nt-group-9-light-bg: #D7FFD9;--nt-group-10-main: #000000;--nt-group-10-dark: #000000;--nt-group-10-light: #000000;--nt-group-10-main-bg: #C5E1A5;--nt-group-10-dark-bg: #94AF76;--nt-group-10-light-bg: #F8FFD7;--nt-group-11-main: #000000;--nt-group-11-dark: #000000;--nt-group-11-light: #000000;--nt-group-11-main-bg: #E6EE9C;--nt-group-11-dark-bg: #B3BC6D;--nt-group-11-light-bg: #FFFFCE;--nt-group-12-main: #000000;--nt-group-12-dark: #000000;--nt-group-12-light: #000000;--nt-group-12-main-bg: #FFF59D;--nt-group-12-dark-bg: #CBC26D;--nt-group-12-light-bg: #FFFFCF;--nt-group-13-main: #000000;--nt-group-13-dark: #000000;--nt-group-13-light: #000000;--nt-group-13-main-bg: #FFE082;--nt-group-13-dark-bg: #CAAE53;--nt-group-13-light-bg: #FFFFB3;--nt-group-14-main: #000000;--nt-group-14-dark: #000000;--nt-group-14-light: #000000;--nt-group-14-main-bg: #FFCC80;--nt-group-14-dark-bg: #CA9B52;--nt-group-14-light-bg: #FFFFB0;--nt-group-15-main: #000000;--nt-group-15-dark: #000000;--nt-group-15-light: #000000;--nt-group-15-main-bg: #FFAB91;--nt-group-15-dark-bg: #C97B63;--nt-group-15-light-bg: #FFDDC1;--nt-group-16-main: #000000;--nt-group-16-dark: #000000;--nt-group-16-light: #000000;--nt-group-16-main-bg: #BCAAA4;--nt-group-16-dark-bg: #8C7B75;--nt-group-16-light-bg: #EFDCD5;--nt-group-17-main: #000000;--nt-group-17-dark: #000000;--nt-group-17-light: #000000;--nt-group-17-main-bg: #EEEEEE;--nt-group-17-dark-bg: #BCBCBC;--nt-group-17-light-bg: #FFFFFF;--nt-group-18-main: #000000;--nt-group-18-dark: #000000;--nt-group-18-light: #000000;--nt-group-18-main-bg: #B0BEC5;--nt-group-18-dark-bg: #808E95;--nt-group-18-light-bg: #E2F1F8}.nt-group-0 .nt-plan-group-summary,.nt-group-0 .nt-timeline-dot{color:var(--nt-group-0-dark);background-color:var(--nt-group-0-dark-bg)}.nt-group-0 .period{color:var(--nt-group-0-main);background-color:var(--nt-group-0-main-bg)}.nt-group-1 .nt-plan-group-summary,.nt-group-1 .nt-timeline-dot{color:var(--nt-group-1-dark);background-color:var(--nt-group-1-dark-bg)}.nt-group-1 .period{color:var(--nt-group-1-main);background-color:var(--nt-group-1-main-bg)}.nt-group-2 .nt-plan-group-summary,.nt-group-2 .nt-timeline-dot{color:var(--nt-group-2-dark);background-color:var(--nt-group-2-dark-bg)}.nt-group-2 .period{color:var(--nt-group-2-main);background-color:var(--nt-group-2-main-bg)}.nt-group-3 .nt-plan-group-summary,.nt-group-3 .nt-timeline-dot{color:var(--nt-group-3-dark);background-color:var(--nt-group-3-dark-bg)}.nt-group-3 .period{color:var(--nt-group-3-main);background-color:var(--nt-group-3-main-bg)}.nt-group-4 .nt-plan-group-summary,.nt-group-4 .nt-timeline-dot{color:var(--nt-group-4-dark);background-color:var(--nt-group-4-dark-bg)}.nt-group-4 .period{color:var(--nt-group-4-main);background-color:var(--nt-group-4-main-bg)}.nt-group-5 .nt-plan-group-summary,.nt-group-5 .nt-timeline-dot{color:var(--nt-group-5-dark);background-color:var(--nt-group-5-dark-bg)}.nt-group-5 .period{color:var(--nt-group-5-main);background-color:var(--nt-group-5-main-bg)}.nt-group-6 .nt-plan-group-summary,.nt-group-6 .nt-timeline-dot{color:var(--nt-group-6-dark);background-color:var(--nt-group-6-dark-bg)}.nt-group-6 .period{color:var(--nt-group-6-main);background-color:var(--nt-group-6-main-bg)}.nt-group-7 .nt-plan-group-summary,.nt-group-7 .nt-timeline-dot{color:var(--nt-group-7-dark);background-color:var(--nt-group-7-dark-bg)}.nt-group-7 .period{color:var(--nt-group-7-main);background-color:var(--nt-group-7-main-bg)}.nt-group-8 .nt-plan-group-summary,.nt-group-8 .nt-timeline-dot{color:var(--nt-group-8-dark);background-color:var(--nt-group-8-dark-bg)}.nt-group-8 .period{color:var(--nt-group-8-main);background-color:var(--nt-group-8-main-bg)}.nt-group-9 .nt-plan-group-summary,.nt-group-9 .nt-timeline-dot{color:var(--nt-group-9-dark);background-color:var(--nt-group-9-dark-bg)}.nt-group-9 .period{color:var(--nt-group-9-main);background-color:var(--nt-group-9-main-bg)}.nt-group-10 .nt-plan-group-summary,.nt-group-10 .nt-timeline-dot{color:var(--nt-group-10-dark);background-color:var(--nt-group-10-dark-bg)}.nt-group-10 .period{color:var(--nt-group-10-main);background-color:var(--nt-group-10-main-bg)}.nt-group-11 .nt-plan-group-summary,.nt-group-11 .nt-timeline-dot{color:var(--nt-group-11-dark);background-color:var(--nt-group-11-dark-bg)}.nt-group-11 .period{color:var(--nt-group-11-main);background-color:var(--nt-group-11-main-bg)}.nt-group-12 .nt-plan-group-summary,.nt-group-12 .nt-timeline-dot{color:var(--nt-group-12-dark);background-color:var(--nt-group-12-dark-bg)}.nt-group-12 .period{color:var(--nt-group-12-main);background-color:var(--nt-group-12-main-bg)}.nt-group-13 .nt-plan-group-summary,.nt-group-13 .nt-timeline-dot{color:var(--nt-group-13-dark);background-color:var(--nt-group-13-dark-bg)}.nt-group-13 .period{color:var(--nt-group-13-main);background-color:var(--nt-group-13-main-bg)}.nt-group-14 .nt-plan-group-summary,.nt-group-14 .nt-timeline-dot{color:var(--nt-group-14-dark);background-color:var(--nt-group-14-dark-bg)}.nt-group-14 .period{color:var(--nt-group-14-main);background-color:var(--nt-group-14-main-bg)}.nt-group-15 .nt-plan-group-summary,.nt-group-15 .nt-timeline-dot{color:var(--nt-group-15-dark);background-color:var(--nt-group-15-dark-bg)}.nt-group-15 .period{color:var(--nt-group-15-main);background-color:var(--nt-group-15-main-bg)}.nt-group-16 .nt-plan-group-summary,.nt-group-16 .nt-timeline-dot{color:var(--nt-group-16-dark);background-color:var(--nt-group-16-dark-bg)}.nt-group-16 .period{color:var(--nt-group-16-main);background-color:var(--nt-group-16-main-bg)}.nt-group-17 .nt-plan-group-summary,.nt-group-17 .nt-timeline-dot{color:var(--nt-group-17-dark);background-color:var(--nt-group-17-dark-bg)}.nt-group-17 .period{color:var(--nt-group-17-main);background-color:var(--nt-group-17-main-bg)}.nt-group-18 .nt-plan-group-summary,.nt-group-18 .nt-timeline-dot{color:var(--nt-group-18-dark);background-color:var(--nt-group-18-dark-bg)}.nt-group-18 .period{color:var(--nt-group-18-main);background-color:var(--nt-group-18-main-bg)}.nt-error{border:2px dashed darkred;padding:0 1rem;background:#faf9ba;color:darkred}.nt-timeline{margin-top:30px}.nt-timeline .nt-timeline-title{font-size:1.1rem;margin-top:0}.nt-timeline .nt-timeline-sub-title{margin-top:0}.nt-timeline .nt-timeline-content{font-size:.8rem;border-bottom:2px dashed #ccc;padding-bottom:1.2rem}.nt-timeline.horizontal .nt-timeline-items{flex-direction:row;overflow-x:scroll}.nt-timeline.horizontal .nt-timeline-items>div{min-width:400px;margin-right:50px}.nt-timeline.horizontal.reverse .nt-timeline-items{flex-direction:row-reverse}.nt-timeline.horizontal.center .nt-timeline-before{background-image:linear-gradient(rgba(252, 70, 107, 0) 0%, rgb(252, 70, 107) 100%);background-repeat:no-repeat;background-size:100% 2px;background-position:0 center}.nt-timeline.horizontal.center .nt-timeline-after{background-image:linear-gradient(180deg, rgb(252, 70, 107) 0%, rgba(252, 70, 107, 0) 100%);background-repeat:no-repeat;background-size:100% 2px;background-position:0 center}.nt-timeline.horizontal.center .nt-timeline-items{background-image:radial-gradient(circle, rgb(63, 94, 251) 0%, rgb(252, 70, 107) 100%);background-repeat:no-repeat;background-size:100% 2px;background-position:0 center}.nt-timeline.horizontal .nt-timeline-dot{left:50%}.nt-timeline.horizontal .nt-timeline-dot:not(.bigger){top:calc(50% - 4px)}.nt-timeline.horizontal .nt-timeline-dot.bigger{top:calc(50% - 15px)}.nt-timeline.vertical .nt-timeline-items{flex-direction:column}.nt-timeline.vertical.reverse .nt-timeline-items{flex-direction:column-reverse}.nt-timeline.vertical.center .nt-timeline-before{background:linear-gradient(rgba(252, 70, 107, 0) 0%, rgb(252, 70, 107) 100%) no-repeat center/2px 100%}.nt-timeline.vertical.center .nt-timeline-after{background:linear-gradient(rgb(252, 70, 107) 0%, rgba(252, 70, 107, 0) 100%) no-repeat center/2px 100%}.nt-timeline.vertical.center .nt-timeline-items{background:radial-gradient(circle, rgb(63, 94, 251) 0%, rgb(252, 70, 107) 100%) no-repeat center/2px 100%}.nt-timeline.vertical.center .nt-timeline-dot{left:calc(50% - 10px)}.nt-timeline.vertical.center .nt-timeline-dot:not(.bigger){top:10px}.nt-timeline.vertical.center .nt-timeline-dot.bigger{left:calc(50% - 20px)}.nt-timeline.vertical.left{padding-left:100px}.nt-timeline.vertical.left .nt-timeline-item{padding-left:70px}.nt-timeline.vertical.left .nt-timeline-sub-title{left:-100px;width:100px}.nt-timeline.vertical.left .nt-timeline-before{background:linear-gradient(rgba(252, 70, 107, 0) 0%, rgb(252, 70, 107) 100%) no-repeat 30px/2px 100%}.nt-timeline.vertical.left .nt-timeline-after{background:linear-gradient(rgb(252, 70, 107) 0%, rgba(252, 70, 107, 0) 100%) no-repeat 30px/2px 100%}.nt-timeline.vertical.left .nt-timeline-items{background:radial-gradient(circle, rgb(63, 94, 251) 0%, rgb(252, 70, 107) 100%) no-repeat 30px/2px 100%}.nt-timeline.vertical.left .nt-timeline-dot{left:21px;top:8px}.nt-timeline.vertical.left .nt-timeline-dot.bigger{top:0px;left:10px}.nt-timeline.vertical.right{padding-right:100px}.nt-timeline.vertical.right .nt-timeline-sub-title{right:-100px;text-align:left;width:100px}.nt-timeline.vertical.right .nt-timeline-item{padding-right:70px}.nt-timeline.vertical.right .nt-timeline-before{background:linear-gradient(rgba(252, 70, 107, 0) 0%, rgb(252, 70, 107) 100%) no-repeat calc(100% - 30px)/2px 100%}.nt-timeline.vertical.right .nt-timeline-after{background:linear-gradient(rgb(252, 70, 107) 0%, rgba(252, 70, 107, 0) 100%) no-repeat calc(100% - 30px)/2px 100%}.nt-timeline.vertical.right .nt-timeline-items{background:radial-gradient(circle, rgb(63, 94, 251) 0%, rgb(252, 70, 107) 100%) no-repeat calc(100% - 30px)/2px 100%}.nt-timeline.vertical.right .nt-timeline-dot{right:21px;top:8px}.nt-timeline.vertical.right .nt-timeline-dot.bigger{top:10px;right:10px}.nt-timeline-items{display:flex;position:relative}.nt-timeline-items>div{min-height:100px;padding-top:2px;padding-bottom:20px}.nt-timeline-before{content:"";height:15px}.nt-timeline-after{content:"";height:60px;margin-bottom:20px}.nt-timeline-sub-title{position:absolute;width:50%;top:4px;font-size:18px;color:var(--nt-color-50)}[data-md-color-scheme=slate] .nt-timeline-sub-title{color:var(--nt-color-51)}.nt-timeline-item{position:relative}.nt-timeline.vertical.center:not(.alternate) .nt-timeline-item{padding-left:calc(50% + 40px)}.nt-timeline.vertical.center:not(.alternate) .nt-timeline-item .nt-timeline-sub-title{left:0;padding-right:40px;text-align:right}.nt-timeline.vertical.center.alternate .nt-timeline-item:nth-child(odd){padding-left:calc(50% + 40px)}.nt-timeline.vertical.center.alternate .nt-timeline-item:nth-child(odd) .nt-timeline-sub-title{left:0;padding-right:40px;text-align:right}.nt-timeline.vertical.center.alternate .nt-timeline-item:nth-child(even){text-align:right;padding-right:calc(50% + 40px)}.nt-timeline.vertical.center.alternate .nt-timeline-item:nth-child(even) .nt-timeline-sub-title{right:0;padding-left:40px;text-align:left}.nt-timeline-dot{position:relative;width:20px;height:20px;border-radius:100%;background-color:#fc5b5b;position:absolute;top:0px;z-index:2;display:flex;justify-content:center;align-items:center;box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);border:3px solid #fff}.nt-timeline-dot:not(.bigger) .icon{font-size:10px}.nt-timeline-dot.bigger{width:40px;height:40px;padding:3px}.nt-timeline-dot .icon{color:#fff}@supports not (-moz-appearance: none){details .nt-timeline.vertical.center.alternate .nt-timeline-item:nth-child(odd) .nt-timeline-sub-title,details .nt-timeline.vertical.center:not(.alternate) .nt-timeline-item .nt-timeline-sub-title{left:-40px}details .nt-timeline.vertical.center.alternate .nt-timeline-item:nth-child(even) .nt-timeline-sub-title{right:-40px}details .nt-timeline.vertical.center .nt-timeline-dot{left:calc(50% - 12px)}details .nt-timeline-dot.bigger{font-size:1rem !important}}.nt-timeline-item:nth-child(0) .nt-timeline-dot{background-color:var(--nt-color-0)}.nt-timeline-item:nth-child(1) .nt-timeline-dot{background-color:var(--nt-color-1)}.nt-timeline-item:nth-child(2) .nt-timeline-dot{background-color:var(--nt-color-2)}.nt-timeline-item:nth-child(3) .nt-timeline-dot{background-color:var(--nt-color-3)}.nt-timeline-item:nth-child(4) .nt-timeline-dot{background-color:var(--nt-color-4)}.nt-timeline-item:nth-child(5) .nt-timeline-dot{background-color:var(--nt-color-5)}.nt-timeline-item:nth-child(6) .nt-timeline-dot{background-color:var(--nt-color-6)}.nt-timeline-item:nth-child(7) .nt-timeline-dot{background-color:var(--nt-color-7)}.nt-timeline-item:nth-child(8) .nt-timeline-dot{background-color:var(--nt-color-8)}.nt-timeline-item:nth-child(9) .nt-timeline-dot{background-color:var(--nt-color-9)}.nt-timeline-item:nth-child(10) .nt-timeline-dot{background-color:var(--nt-color-10)}.nt-timeline-item:nth-child(11) .nt-timeline-dot{background-color:var(--nt-color-11)}.nt-timeline-item:nth-child(12) .nt-timeline-dot{background-color:var(--nt-color-12)}.nt-timeline-item:nth-child(13) .nt-timeline-dot{background-color:var(--nt-color-13)}.nt-timeline-item:nth-child(14) .nt-timeline-dot{background-color:var(--nt-color-14)}.nt-timeline-item:nth-child(15) .nt-timeline-dot{background-color:var(--nt-color-15)}.nt-timeline-item:nth-child(16) .nt-timeline-dot{background-color:var(--nt-color-16)}.nt-timeline-item:nth-child(17) .nt-timeline-dot{background-color:var(--nt-color-17)}.nt-timeline-item:nth-child(18) .nt-timeline-dot{background-color:var(--nt-color-18)}.nt-timeline-item:nth-child(19) .nt-timeline-dot{background-color:var(--nt-color-19)}.nt-timeline-item:nth-child(20) .nt-timeline-dot{background-color:var(--nt-color-20)}:root{--nt-scrollbar-color: #2751b0;--nt-plan-actions-height: 24px;--nt-units-background: #ff9800;--nt-months-background: #2751b0;--nt-plan-vertical-line-color: #a3a3a3ad}.nt-pastello{--nt-scrollbar-color: #9fb8f4;--nt-units-background: #f5dc82;--nt-months-background: #5b7fd1}[data-md-color-scheme=slate]{--nt-units-background: #003773}[data-md-color-scheme=slate] .nt-pastello{--nt-units-background: #3f4997}.nt-plan-root{min-height:200px;scrollbar-width:20px;scrollbar-color:var(--nt-scrollbar-color);display:flex}.nt-plan-root ::-webkit-scrollbar{width:20px}.nt-plan-root ::-webkit-scrollbar-track{box-shadow:inset 0 0 5px gray;border-radius:10px}.nt-plan-root ::-webkit-scrollbar-thumb{background:var(--nt-scrollbar-color);border-radius:10px}.nt-plan-root .nt-plan{flex:80%}.nt-plan-root.no-groups .nt-plan-periods{padding-left:0}.nt-plan-root.no-groups .nt-plan-group-summary{display:none}.nt-plan-root .nt-timeline-dot.bigger{top:-10px}.nt-plan-root .nt-timeline-dot.bigger[title]{cursor:help}.nt-plan{white-space:nowrap;overflow-x:auto;display:flex}.nt-plan .ug-timeline-dot{left:368px;top:-8px;cursor:help}.months{display:flex}.month{flex:auto;display:inline-block;box-shadow:rgba(0,0,0,.2) 0px 3px 1px -2px,rgba(0,0,0,.14) 0px 2px 2px 0px,rgba(0,0,0,.12) 0px 1px 5px 0px inset;background-color:var(--nt-months-background);color:#fff;text-transform:uppercase;font-family:Roboto,Helvetica,Arial,sans-serif;padding:2px 5px;font-size:12px;border:1px solid #000;width:150px;border-radius:8px}.nt-plan-group-activities{flex:auto;position:relative}.nt-vline{border-left:1px dashed var(--nt-plan-vertical-line-color);height:100%;left:0;position:absolute;margin-left:-0.5px;top:0;-webkit-transition:all .5s linear !important;-moz-transition:all .5s linear !important;-ms-transition:all .5s linear !important;-o-transition:all .5s linear !important;transition:all .5s linear !important;z-index:-2}.nt-plan-activity{display:flex;margin:2px 0;background-color:rgba(187,187,187,.2509803922)}.actions{height:var(--nt-plan-actions-height)}.actions{position:relative}.period{display:inline-block;height:var(--nt-plan-actions-height);width:120px;position:absolute;left:0px;background:#1da1f2;border-radius:5px;transition:all .5s;cursor:help;-webkit-transition:width 1s ease-in-out;-moz-transition:width 1s ease-in-out;-o-transition:width 1s ease-in-out;transition:width 1s ease-in-out}.period .nt-tooltip{display:none;top:30px;position:relative;padding:1rem;text-align:center;font-size:12px}.period:hover .nt-tooltip{display:inline-block}.period-0{left:340px;visibility:visible;background-color:#456165}.period-1{left:40px;visibility:visible;background-color:green}.period-2{left:120px;visibility:visible;background-color:pink;width:80px}.period-3{left:190px;visibility:visible;background-color:darkred;width:150px}.weeks>span,.days>span{height:25px}.weeks>span{display:inline-block;margin:0;padding:0;font-weight:bold}.weeks>span .week-text{font-size:10px;position:absolute;display:inline-block;padding:3px 4px}.days{z-index:-2;position:relative}.day-text{font-size:10px;position:absolute;display:inline-block;padding:3px 4px}.period span{font-size:12px;vertical-align:top;margin-left:4px;color:#000;background:rgba(255,255,255,.6588235294);border-radius:6px;padding:0 4px}.weeks,.days{height:20px;display:flex;box-sizing:content-box}.months{display:flex}.week,.day{height:20px;position:relative;border:1;flex:auto;border:2px solid #fff;border-radius:4px;background-color:var(--nt-units-background);cursor:help}.years{display:flex}.year{text-align:center;border-right:1px solid var(--nt-plan-vertical-line-color);font-weight:bold}.year:first-child{border-left:1px solid var(--nt-plan-vertical-line-color)}.year:first-child:last-child{width:100%}.quarters{display:flex}.quarter{width:12.5%;text-align:center;border-right:1px solid var(--nt-plan-vertical-line-color);font-weight:bold}.quarter:first-child{border-left:1px solid var(--nt-plan-vertical-line-color)}.nt-plan-group{margin:20px 0;position:relative}.nt-plan-group{display:flex}.nt-plan-group-summary{background:#2751b0;width:150px;white-space:normal;padding:.1rem .5rem;border-radius:5px;color:#fff;z-index:3}.nt-plan-group-summary p{margin:0;padding:0;font-size:.6rem;color:#fff}.nt-plan-group-summary,.month,.period,.week,.day,.nt-tooltip{border:3px solid #fff;box-shadow:0 2px 3px -1px rgba(0,0,0,.2),0 3px 3px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12)}.nt-plan-periods{padding-left:150px}.months{z-index:2;position:relative}.weeks{position:relative;top:-2px;z-index:0}.month,.quarter,.year,.week,.day,.nt-tooltip{font-family:Roboto,Helvetica,Arial,sans-serif;box-sizing:border-box}.nt-cards.nt-grid{display:grid;grid-auto-columns:1fr;gap:.5rem;max-width:100vw;overflow-x:auto;padding:1px}.nt-cards.nt-grid.cols-1{grid-template-columns:repeat(1, 1fr)}.nt-cards.nt-grid.cols-2{grid-template-columns:repeat(2, 1fr)}.nt-cards.nt-grid.cols-3{grid-template-columns:repeat(3, 1fr)}.nt-cards.nt-grid.cols-4{grid-template-columns:repeat(4, 1fr)}.nt-cards.nt-grid.cols-5{grid-template-columns:repeat(5, 1fr)}.nt-cards.nt-grid.cols-6{grid-template-columns:repeat(6, 1fr)}@media only screen and (max-width: 400px){.nt-cards.nt-grid{grid-template-columns:repeat(1, 1fr) !important}}.nt-card{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.nt-card:hover{box-shadow:0 2px 2px 0 rgba(0,0,0,.24),0 3px 1px -2px rgba(0,0,0,.3),0 1px 5px 0 rgba(0,0,0,.22)}[data-md-color-scheme=slate] .nt-card{box-shadow:0 2px 2px 0 rgba(4,40,33,.14),0 3px 1px -2px rgba(40,86,94,.47),0 1px 5px 0 rgba(139,252,255,.64)}[data-md-color-scheme=slate] .nt-card:hover{box-shadow:0 2px 2px 0 rgba(0,255,206,.14),0 3px 1px -2px rgba(33,156,177,.47),0 1px 5px 0 rgba(96,251,255,.64)}.nt-card>a{color:var(--md-default-fg-color)}.nt-card>a>div{cursor:pointer}.nt-card{padding:5px;margin-bottom:.5rem}.nt-card-title{font-size:1rem;font-weight:bold;margin:4px 0 8px 0;line-height:22px}.nt-card-content{padding:.4rem .8rem .8rem .8rem}.nt-card-text{font-size:14px;padding:0;margin:0}.nt-card .nt-card-image{text-align:center;border-radius:2px;background-position:center center;background-size:cover;background-repeat:no-repeat;min-height:120px}.nt-card .nt-card-image.tags img{margin-top:12px}.nt-card .nt-card-image img{height:105px;margin-top:5px}.nt-card a:hover,.nt-card a:focus{color:var(--md-accent-fg-color)}.nt-card h2{margin:0}.span-table-wrapper table{border-collapse:collapse;margin-bottom:2rem;border-radius:.1rem}.span-table td,.span-table th{padding:.2rem;background-color:var(--md-default-bg-color);font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto;border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.span-table tr:first-child td{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.span-table td:first-child{border-left:.05rem solid var(--md-typeset-table-color)}.span-table td:last-child{border-right:.05rem solid var(--md-typeset-table-color)}.span-table tr:last-child{border-bottom:.05rem solid var(--md-typeset-table-color)}.span-table [colspan],.span-table [rowspan]{font-weight:bold;border:.05rem solid var(--md-typeset-table-color)}.span-table tr:not(:first-child):hover td:not([colspan]):not([rowspan]),.span-table td[colspan]:hover,.span-table td[rowspan]:hover{background-color:rgba(0,0,0,.035);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset;transition:background-color 125ms}.nt-contribs{margin-top:2rem;font-size:small;border-top:1px dotted #d3d3d3;padding-top:.5rem}.nt-contribs .nt-contributors{padding-top:.5rem;display:flex;flex-wrap:wrap}.nt-contribs .nt-contributor{background:#d3d3d3;background-size:cover;width:40px;height:40px;border-radius:100%;margin:0 6px 6px 0;cursor:help;opacity:.7}.nt-contribs .nt-contributor:hover{opacity:1}.nt-contribs .nt-contributors-title{font-style:italic;margin-bottom:0}.nt-contribs .nt-initials{text-transform:uppercase;font-size:24px;text-align:center;width:40px;height:40px;display:inline-block;vertical-align:middle;position:relative;top:2px;color:inherit;font-weight:bold}.nt-contribs .nt-group-0{background-color:var(--nt-color-0)}.nt-contribs .nt-group-1{background-color:var(--nt-color-1)}.nt-contribs .nt-group-2{background-color:var(--nt-color-2)}.nt-contribs .nt-group-3{background-color:var(--nt-color-3)}.nt-contribs .nt-group-4{background-color:var(--nt-color-4)}.nt-contribs .nt-group-5{background-color:var(--nt-color-5)}.nt-contribs .nt-group-6{background-color:var(--nt-color-6)}.nt-contribs .nt-group-7{color:#000;background-color:var(--nt-color-7)}.nt-contribs .nt-group-8{color:#000;background-color:var(--nt-color-8)}.nt-contribs .nt-group-9{background-color:var(--nt-color-9)}.nt-contribs .nt-group-10{background-color:var(--nt-color-10)}.nt-contribs .nt-group-11{background-color:var(--nt-color-11)}.nt-contribs .nt-group-12{background-color:var(--nt-color-12)}.nt-contribs .nt-group-13{background-color:var(--nt-color-13)}.nt-contribs .nt-group-14{background-color:var(--nt-color-14)}.nt-contribs .nt-group-15{color:#000;background-color:var(--nt-color-15)}.nt-contribs .nt-group-16{background-color:var(--nt-color-16)}.nt-contribs .nt-group-17{color:#000;background-color:var(--nt-color-17)}.nt-contribs .nt-group-18{background-color:var(--nt-color-18)}.nt-contribs .nt-group-19{background-color:var(--nt-color-19)}.nt-contribs .nt-group-20{color:#000;background-color:var(--nt-color-20)}.nt-contribs .nt-group-21{color:#000;background-color:var(--nt-color-21)}.nt-contribs .nt-group-22{color:#000;background-color:var(--nt-color-22)}.nt-contribs .nt-group-23{color:#000;background-color:var(--nt-color-23)}.nt-contribs .nt-group-24{color:#000;background-color:var(--nt-color-24)}.nt-contribs .nt-group-25{color:#000;background-color:var(--nt-color-25)}.nt-contribs .nt-group-26{color:#000;background-color:var(--nt-color-26)}.nt-contribs .nt-group-27{background-color:var(--nt-color-27)}.nt-contribs .nt-group-28{color:#000;background-color:var(--nt-color-28)}.nt-contribs .nt-group-29{color:#000;background-color:var(--nt-color-29)}.nt-contribs .nt-group-30{background-color:var(--nt-color-30)}.nt-contribs .nt-group-31{background-color:var(--nt-color-31)}.nt-contribs .nt-group-32{color:#000;background-color:var(--nt-color-32)}.nt-contribs .nt-group-33{background-color:var(--nt-color-33)}.nt-contribs .nt-group-34{background-color:var(--nt-color-34)}.nt-contribs .nt-group-35{background-color:var(--nt-color-35)}.nt-contribs .nt-group-36{background-color:var(--nt-color-36)}.nt-contribs .nt-group-37{background-color:var(--nt-color-37)}.nt-contribs .nt-group-38{background-color:var(--nt-color-38)}.nt-contribs .nt-group-39{color:#000;background-color:var(--nt-color-39)}.nt-contribs .nt-group-40{color:#000;background-color:var(--nt-color-40)}.nt-contribs .nt-group-41{color:#000;background-color:var(--nt-color-41)}.nt-contribs .nt-group-42{color:#000;background-color:var(--nt-color-42)}.nt-contribs .nt-group-43{color:#000;background-color:var(--nt-color-43)}.nt-contribs .nt-group-44{color:#000;background-color:var(--nt-color-44)}.nt-contribs .nt-group-45{background-color:var(--nt-color-45)}.nt-contribs .nt-group-46{color:#000;background-color:var(--nt-color-46)}.nt-contribs .nt-group-47{background-color:var(--nt-color-47)}.nt-contribs .nt-group-48{background-color:var(--nt-color-48)}.nt-contribs .nt-group-49{background-color:var(--nt-color-49)} diff --git a/rodi/docs/getting-started.md b/rodi/docs/getting-started.md new file mode 100644 index 0000000..ccd369f --- /dev/null +++ b/rodi/docs/getting-started.md @@ -0,0 +1,492 @@ +# Getting started with Rodi + +This page describes the basics to start using Rodi. It provides: + +- [X] An overview of dependency injection. +- [X] The use cases `Rodi` is intended for. +- [X] Examples of real-life scenarios. + +## Overview of dependency injection + +Consider the following example: + +```python +class A: + ... + + +class B: + def __init__(self, dependency: A): + self.dependency = dependency +``` + +The type `B` depends upon the type `A`, because it requires an instance of `A` +in its constructor. In other words, `A` is a _dependency_ of `B`. + +For a more concrete example, consider the following: + +```python +class ProductsRepository: + """Provides methods to read, write, and delete products information.""" + + +class ProductsService: + """Provides business logic for managing products.""" + + def __init__(self, repository: ProductsRepository): + self.repository = repository +``` + +The `ProductsService` requires an instance of `ProductsRepository`. The former +handles business logic, while the latter defines a type responsible for +storing, reading, and deleting product information. + +Imagine we also need to send emails when certain events happen, the +`ProductsService` would likely have an additional dependency: + +```python +class ProductsService: + """Provides business logic for managing products.""" + + def __init__( + self, + repository: ProductsRepository, + email_handler: EmailHandler + ): + self.repository = repository + self.email_handler = email_handler +``` + +Encapsulating the code that for data access operations (`ProductsRepository`) +and that sends emails (`EmailHandler`) into dedicated classes is the right +approach, as the same functionality can be reused in other services (e.g., +_OrdersService, AccountsService_) without duplicating logic or tightly coupling +it to other classes. + +--- + +The dependencies _could also_ be instantiated by the class that needs them: + +```python +class ProductsService: + """Provides business logic for managing products.""" + + def __init__(self): + self.repository = ProductsRepository() + self.email_handler = EmailHandler() +``` + +However, this approach has several limitations. + +- **Scalability Issues**: As the application grows, managing dependencies + manually within classes becomes cumbersome. It can lead to duplicated code + and make the system harder to maintain. As dependencies are likely to require + their own set of parameters passed to their constructors, the parent + constructor would become more and more complex. +- **Tight Coupling**: The `ProductsService` class is tightly coupled to + _concrete_ implementations of its dependencies. This makes it less convenient + to replace `ProductsRepository` and `EmailHandler` with different + implementations (e.g., a mock for testing or a different database backend). +- **Reduced Testability**: Since dependencies are instantiated within the + class, it is necessary to modify the properties of instances of + `ProductsService`, to replace them with mocks or stubs during unit testing. +- **Lack of Flexibility**: If the application needs to use a different + implementation of `ProductsRepository` (e.g., for different environments or + configurations), the source code of the `ProductsService` class must be + modified. +- **Code Duplication**: If multiple classes need the same dependency, each + class would need to instantiate it, leading to duplicated code and increased + maintenance overhead. +- **Configuration Management**: Managing configuration settings (e.g., database + connection strings or API keys) becomes harder because they are scattered + across multiple classes instead of being centralized. +- **Runtime Flexibility**: Instantiating dependencies directly in the class + makes it harder to dynamically change or configure dependencies at runtime + (e.g., switching to a different implementation based on environment + variables). + +Alternatively, dependencies could be instantiated at the module level and +managed as global variables. +Instantiating dependencies as globals at module level is generally not ideal, +as it leads to: + +- **Tight Coupling to Global State**: When dependencies are global, any part of + the application can access and modify them. This makes the code tightly + coupled to the global state, leading to unpredictable behavior and bugs that + are hard to trace. +- **Reduced Testability**: Global dependencies make unit testing difficult + because tests cannot easily isolate or mock dependencies. Each test might + inadvertently affect or be affected by the global state, leading to flaky + tests. +- **Lack of Flexibility**: If you need to use different implementations of a + dependency (e.g., for testing, staging, or production environments), global + variables make it more cumbersome to switch implementations dynamically. + +Now that we have arguments for _not_ instantiating dependencies inside the +classes that need them and for not instantiating them as global variables, +let's see how dependency injection can help addressing the problems listed +above. + +### Inversion of Control + +**Inversion of Control (IoC)** is a design principle in which the control of +object creation and dependency management is inverted from the class itself to +an external entity, such as a framework or container. Instead of a class +instantiating its dependencies directly, they are provided to the class from +the outside. This promotes loose coupling and enhances testability. +**Dependency Injection** is a common implementation of IoC. + +### Dependency Injection + +**Dependency Injection** is a design principle where a class does not create +its own dependencies. Instead, the dependencies are provided (or "injected") +into the class from the outside. This makes the class more flexible, easier to +test, and less dependent on specific implementations. + +If we consider again the classes `A` and `B` described earlier, they can be +registered and resolved using `rodi` this way: + +```python +# example1.py +class A: + ... + + +class B: + def __init__(self, dependency: A): + self.dependency = dependency +``` + +```python +# main.py +from example1 import A, B + +from rodi import Container + + +container = Container() + +# register types: +container.add_transient(A) +container.add_transient(B) + +# resolve B +example = container.resolve(B) + +# the container automatically resolves +assert isinstance(example, B) +assert isinstance(example.dependency, A) +``` + +/// admonition | Completely non-intrusive + type: tip + +Note how `rodi` is completely non-intrusive and does **not** require changing +the source code of the types it handles. This was one of the objectives of the +library. +/// + +In this example, both `A` and `B` are concrete types. Rodi can resolve concrete +types without any issues. However, the true power of dependency injection +becomes evident when we use _abstract types_ or _interfaces_ to define +dependencies. Let's talk about the _Dependency Inversion Principle_. + +### Dependency Inversion Principle + +The **Dependency Inversion Principle (DIP)** is a design principle that says +high-level modules (like business logic) should not depend on low-level modules +(like database access). Instead, _both_ should depend on abstractions, like +interfaces or abstract classes. This makes the code more flexible and easier +to change because you can swap out the low-level details without affecting the +high-level logic. Inversion of Control aligns with the Dependency Inversion +Principle. + +Consider the following example, of `ProductsService`, `ProductsRepository`, +and `SQLProductsRepository`. + +```python +# domain/products.py +from abc import ABC, abstractmethod +from dataclasses import dataclass + + +@dataclass +class Product: + id: int + name: str + description: str + price: float + + +# Abstraction: ProductsRepository +class ProductsRepository(ABC): + """ + Abstract base class for product repositories. + Defines the interface for data access operations. + """ + + @abstractmethod + def get_all_products(self) -> list[Product]: + """Retrieve all products.""" + + @abstractmethod + def get_product_by_id(self, product_id: int) -> Product | None: + """Retrieve a product by its ID.""" + + @abstractmethod + def create_product(self, product: Product) -> int: + """Create a new product.""" + + +# High-level module: ProductsService +class ProductsService: + """ + Provides business logic for managing products. + Depends on an abstract ProductsRepository. + """ + + def __init__(self, repository: ProductsRepository): + self.repository = repository + + def get_all_products(self) -> list[Product]: + """Retrieve all products.""" + return self.repository.get_all_products() + + def get_product_by_id(self, product_id: int) -> Product | None: + """Retrieve a product by its ID.""" + return self.repository.get_product_by_id(product_id) + + def create_product(self, product: Product) -> int: + """Create a new product.""" + return self.repository.create_product(product) + +``` + +```python +# data/sql/products.py +from domain.products import Product, ProductsRepository + + +# Low-level module: SQLProductsRepository +class SQLProductsRepository(ProductsRepository): + """ + Concrete implementation of ProductsRepository using a SQL database. + """ + + def __init__(self, db_connection): + self.db_connection = db_connection + + def get_all_products(self) -> list[Product]: + """Retrieve all products from the database.""" + cursor = self.db_connection.cursor() + cursor.execute("SELECT id, name, description, price FROM products") + rows = cursor.fetchall() + return [ + Product(id=row[0], name=row[1], description=row[2], price=row[3]) + for row in rows + ] + + def get_product_by_id(self, product_id: int) -> Product | None: + """Retrieve a product by its ID.""" + cursor = self.db_connection.cursor() + cursor.execute( + "SELECT id, name, description, price FROM products WHERE id = ?", + (product_id,), + ) + row = cursor.fetchone() + if row: + return Product(id=row[0], name=row[1], description=row[2], price=row[3]) + return None + + def create_product(self, product: Product) -> int: + """Insert a new product into the database.""" + cursor = self.db_connection.cursor() + cursor.execute( + "INSERT INTO products (name, description, price) VALUES (?, ?, ?)", + (product.name, product.description, product.price), + ) + self.db_connection.commit() + return cursor.lastrowid +``` + +**Explanation:** + +- The **abstraction `ProductsRepository`** defines the interface for data + access operations. +- The **high-level class (`ProductsService`)** depends on this abstraction, not + on concrete implementations. +- The **high-level class (`ProductsService`)** implements business logic and + depends on the `ProductsRepository` abstraction. +- `ProductsService` does not depend on the details of how data is stored or + retrieved. +- The low-level class (`SQLProductsRepository`) implements the + `ProductsRepository` interface using an SQL database. +- It can be swapped out for another implementation (e.g., + `InMemoryProductsRepository`) without modifying the `ProductsService`. + +```mermaid +classDiagram + class ProductsRepository { + <> + +get_all_products() list~Product~ + +get_product_by_id(product_id: int) Product | None + +create_product(product: Product) int + } + + class SQLProductsRepository { + +get_all_products() list~Product~ + +get_product_by_id(product_id: int) Product | None + +create_product(product: Product) int + } + + class ProductsService { + -repository: ProductsRepository + +get_all_products() list~Product~ + +get_product_by_id(product_id: int) Product | None + +create_product(product: Product) int + } + + ProductsRepository <-- SQLProductsRepository : implements + ProductsService --> ProductsRepository : depends on +``` + +The benefits of DIP are: + +- **Loose Coupling**: The `ProductsService` is decoupled from the specific + implementation of the repository. +- **Flexibility**: You can easily replace `SQLProductsRepository` with another + implementation (e.g., a mock for testing). +- **Testability**: The `ProductsService` can be tested independently by + injecting a mock or stub implementation of `ProductsRepository`. + +To better understand the concept, consider the following example that shows how +those classes can be imported and instantiated: + +```python +import sqlite3 + +from data.sql.products import SQLProductsRepository +from domain.products import Product, ProductsService + +# Set up an SQLite database connection +connection = sqlite3.connect(":memory:") +connection.execute( + """ + CREATE TABLE products ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + price REAL NOT NULL + ) + """ +) + +# Instantiate the low-level module (SQLProductsRepository) +sql_repository = SQLProductsRepository(connection) + +# Instantiate the high-level module (ProductsService) +service = ProductsService(sql_repository) + +# Use the service +new_product = Product( + id=0, name="Laptop", description="A powerful laptop", price=1200.00 +) +product_id = service.create_product(new_product) +print(service.get_product_by_id(product_id)) +print(service.get_all_products()) +``` + +As the number of dependencies grow, the code that instantiates objects can +easily become hard to maintain. To simplify the management of dependencies and +reduce the complexity of object instantiation, we can leverage a dependency +injection framework like `rodi`. + +## The Repository pattern example + +The three classes described above: `ProductsService`, `ProductsRepository`, and +`SQLProductsRepository`, can be wired using `rodi` this way: + +```python {linenums="1"} +import sqlite3 + +from rodi import Container + +from data.sql.products import SQLProductsRepository +from domain.products import Product, ProductsRepository, ProductsService + + +container = Container() + + +def connection_factory() -> sqlite3.Connection: + """Create a new SQLite database connection.""" + conn = sqlite3.connect(":memory:") + conn.execute( + """ + CREATE TABLE products ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + price REAL NOT NULL + ) + """ + ) + return conn + + +container.add_transient_by_factory(connection_factory) +container.add_alias("db_connection", sqlite3.Connection) + +container.add_transient(ProductsRepository, SQLProductsRepository) +container.add_transient(ProductsService) + + +# Obtain an instance of the service +service = container.resolve(ProductsService) + +# Use the service +new_product = Product( + id=0, name="Laptop", description="A powerful laptop", price=1200.00 +) +product_id = service.create_product(new_product) +print(service.get_product_by_id(product_id)) +print(service.get_all_products()) +``` + +Some interesting things are happening in this code: + +- At line _9_, an instance of `rodi.Container` is created. This class is used + to register the types that must be resolved, and resolve those types. +- It was not necessary to modify the source code of the classes being handled: + `rodi` inspects the code of registered types to know how to resolve them. +- A factory function is used to define how the instance of `sqlite3.Connection` + is to be created. This is convenient because the `connect` method, which + returns an instance of that class, requires a `str`, and resolving base types + with `DI` is not a good idea. +- The factory has a return type annotation: `rodi` uses that type annotation + as the _key type_ to be resolved using the factory function. +- Because the constructor of the `SQLProductsRepository` class did not include + a type annotation to describe its dependency `db_connection`, an alias is + configured at line _29_, to instruct `rodi` to resolve parameters having + name `db_connection` to obtain an instance of `sqlite3.Connection`. + Alternatively, we could have updated the source code of + `SQLProductsRepository` to include a type annotation in its constructor. +- At line _31_, the **abstract** type `ProductsRepository` is registered, + instructing the container to resolve that type with the **concrete** + implementation `SQLProductsRepository`. According to the **DIP** principle, + when registering an abstract type and its implementation, `rodi` requires + using the abstract type as _key_. +- At line _32_, the `ProductsService` type is also registered, because this is + required to build the graph of dependencies. +- At line _36_, an instance of `ProductsService` it obtained through **DI**. + Since this is the first time the `Container` needs to resolve a type, it runs + code inspections to build the tree of dependencies. These code inspections + are executed only once, unless new types are registered in the same + `Container`. The container obtains all necessary objects: from the + `db_connection` and the `SQLProductsRepository` to resolve the **abstract** + dependency `ProductsRepository`, used to instantiate the requested + `ProductsService`. + +## Summary + +... diff --git a/rodi/docs/img/neoteroi-w.svg b/rodi/docs/img/neoteroi-w.svg new file mode 100644 index 0000000..45fd9e7 --- /dev/null +++ b/rodi/docs/img/neoteroi-w.svg @@ -0,0 +1,74 @@ + + + + + + + + image/svg+xml + + + + + + + + diff --git a/rodi/docs/img/neoteroi.ico b/rodi/docs/img/neoteroi.ico new file mode 100644 index 0000000000000000000000000000000000000000..11cd8148480e2e78fc3647eaac8dc6f32448419b GIT binary patch literal 305984 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 sz4$i$3*pLzTTxOPY3Dp&y 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 CZ0?8v literal 0 HcmV?d00001 diff --git a/rodi/docs/index.md b/rodi/docs/index.md new file mode 100644 index 0000000..6a435d5 --- /dev/null +++ b/rodi/docs/index.md @@ -0,0 +1,29 @@ +--- +title: Rodi - Dependency Injection for Python +no_comments: true +--- + +# Rodi is a dependency injection container for Python + +```shell +pip install rodi +``` + +## Rodi offers... + +- A **non-intrusive** implementation of dependency injection, that does not + require modifying the classes it handles (no decorators are needed, no + changes in injected classes). +- A strategy to better organize source code, reduce code repetition, and + improve development experience. +- Simplified dependency management with automatic resolution and injection of + dependencies, by type annotation in constructors or type properties. +- A fast implementation that performs code inspections only once, rather than + at each type resolution. +- A generic code API that can be used with any kind of Python applications. + +## Getting started + +To get started with Rodi, read the _Getting Started_ page: + +- [Basics](./getting-started.md) diff --git a/rodi/docs/js/fullscreen.js b/rodi/docs/js/fullscreen.js new file mode 100644 index 0000000..7d18c4d --- /dev/null +++ b/rodi/docs/js/fullscreen.js @@ -0,0 +1,26 @@ +document.addEventListener("DOMContentLoaded", function () { + function setFullScreen() { + localStorage.setItem("FULLSCREEN", "Y") + document.documentElement.classList.add("fullscreen"); + } + function exitFullScreen() { + localStorage.setItem("FULLSCREEN", "N") + document.documentElement.classList.remove("fullscreen"); + } + + // Select all radio inputs with the name "__fullscreen" + const fullscreenRadios = document.querySelectorAll('input[name="__fullscreen"]'); + + // Add a change event listener to each radio input + fullscreenRadios.forEach(function (radio) { + radio.addEventListener("change", function () { + if (radio.checked) { + if (radio.id === "__fullscreen") { + setFullScreen(); + } else if (radio.id === "__fullscreen_no") { + exitFullScreen(); + } + } + }); + }); +}); diff --git a/rodi/mkdocs.yml b/rodi/mkdocs.yml new file mode 100644 index 0000000..4d8d3ec --- /dev/null +++ b/rodi/mkdocs.yml @@ -0,0 +1,76 @@ +site_name: Rodi +site_author: Roberto Prevato +site_description: Rodi, an implementation of Dependency Injection container for Python +site_url: https://www.neoteroi.dev/rodi/ +repo_name: Neoteroi/Rodi +repo_url: https://github.com/Neoteroi/rodi +edit_uri: "" + +nav: + - Overview: index.md + - "Getting started": getting-started.md + - Neoteroi docs home: "/" + +theme: + features: + - navigation.footer + - content.code.copy + - content.action.view + palette: + - scheme: slate + toggle: + icon: material/toggle-switch + name: Switch to light mode + - scheme: default + toggle: + icon: material/toggle-switch-off-outline + name: Switch to dark mode + name: "material" + custom_dir: overrides/ + # highlightjs: true # ? + favicon: img/neoteroi.ico + logo: img/neoteroi-w.svg + icon: + repo: fontawesome/brands/github + +extra: + header_bg_color: "teal" # "#51003c" + +extra_css: + - css/neoteroi.css + - css/extra.css?v=20221120 + +extra_javascript: + - js/fullscreen.js + +plugins: + - search + - neoteroi.contribs + +markdown_extensions: +# - markdown.extensions.codehilite: +# linenums: true +# guess_lang: false + - pymdownx.highlight: + use_pygments: true + guess_lang: false + anchor_linenums: true + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + - pymdownx.blocks.admonition + - pymdownx.blocks.details + - neoteroi.timeline + - neoteroi.cards + - neoteroi.projects + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg diff --git a/rodi/overrides/main.html b/rodi/overrides/main.html new file mode 100644 index 0000000..f51ad86 --- /dev/null +++ b/rodi/overrides/main.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% block extrahead %} + {% set title = config.site_name %} + {% if page and page.title and not page.is_homepage %} + {% set title = config.site_name ~ " - " ~ page.title | striptags %} + {% endif %} + {% set image = config.site_url ~ 'img/banner.png' %} + + + + + + + + + + + + + + + +{% endblock %} +{% block content %} + {{ super() }} +{% endblock %} +{% block analytics %} + + +{% endblock %} diff --git a/rodi/overrides/partials/comments.html b/rodi/overrides/partials/comments.html new file mode 100644 index 0000000..c62c877 --- /dev/null +++ b/rodi/overrides/partials/comments.html @@ -0,0 +1,49 @@ +{% if not page.meta.no_comments %} + + + + + +{% endif %} diff --git a/rodi/overrides/partials/content.html b/rodi/overrides/partials/content.html new file mode 100644 index 0000000..63fa92f --- /dev/null +++ b/rodi/overrides/partials/content.html @@ -0,0 +1,16 @@ +{% if "tags" in config.plugins %} + {% include "partials/tags.html" %} +{% endif %} +{% include "partials/actions.html" %} +{% if not "\x3ch1" in page.content %} +

{{ page.title | d(config.site_name, true)}}

+{% endif %} +{{ page.content }} +{% if page.meta and ( + page.meta.git_revision_date_localized or + page.meta.revision_date +) %} + {% include "partials/source-file.html" %} +{% endif %} +{% include "partials/feedback.html" %} +{% include "partials/comments.html" %} diff --git a/rodi/overrides/partials/header.html b/rodi/overrides/partials/header.html new file mode 100644 index 0000000..4842e4c --- /dev/null +++ b/rodi/overrides/partials/header.html @@ -0,0 +1,76 @@ +{#- + This file was automatically generated - do not edit +-#} +{% set class = "md-header" %} +{% if "navigation.tabs.sticky" in features %} + {% set class = class ~ " md-header--shadow md-header--lifted" %} +{% elif "navigation.tabs" not in features %} + {% set class = class ~ " md-header--shadow" %} +{% endif %} +
+ + {% if "navigation.tabs.sticky" in features %} + {% if "navigation.tabs" in features %} + {% include "partials/tabs.html" %} + {% endif %} + {% endif %} +
From b7f162a21eb4efa51e325842893a0f6af1a6ccd9 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Wed, 9 Apr 2025 05:59:00 +0200 Subject: [PATCH 02/13] Update extra.css --- rodi/docs/css/extra.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rodi/docs/css/extra.css b/rodi/docs/css/extra.css index 468ae82..8720e62 100644 --- a/rodi/docs/css/extra.css +++ b/rodi/docs/css/extra.css @@ -3,7 +3,7 @@ } [data-md-color-scheme=default] { - --md-code-hl-comment-color: #ab0404; + --md-code-hl-comment-color: #b91414; /* #ab0404; */ } html { From 96d6353489fa4b28cb7650046246ffb78bfdb9b6 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Wed, 9 Apr 2025 23:16:51 +0200 Subject: [PATCH 03/13] WIP --- rodi/docs/container-protocol.md | 1 + rodi/docs/getting-started.md | 248 +++++++++++++++++++++++++++++--- rodi/docs/index.md | 8 +- rodi/docs/types-lifetime.md | 147 +++++++++++++++++++ rodi/mkdocs.yml | 11 +- 5 files changed, 386 insertions(+), 29 deletions(-) create mode 100644 rodi/docs/container-protocol.md create mode 100644 rodi/docs/types-lifetime.md diff --git a/rodi/docs/container-protocol.md b/rodi/docs/container-protocol.md new file mode 100644 index 0000000..9fa5192 --- /dev/null +++ b/rodi/docs/container-protocol.md @@ -0,0 +1 @@ +The container protocol... diff --git a/rodi/docs/getting-started.md b/rodi/docs/getting-started.md index ccd369f..fb8bcb8 100644 --- a/rodi/docs/getting-started.md +++ b/rodi/docs/getting-started.md @@ -1,10 +1,9 @@ # Getting started with Rodi -This page describes the basics to start using Rodi. It provides: +This page introduces the basics of using Rodi, including: - [X] An overview of dependency injection. - [X] The use cases `Rodi` is intended for. -- [X] Examples of real-life scenarios. ## Overview of dependency injection @@ -57,15 +56,14 @@ class ProductsService: self.email_handler = email_handler ``` -Encapsulating the code that for data access operations (`ProductsRepository`) +Encapsulating the code that performs data access operations (`ProductsRepository`) and that sends emails (`EmailHandler`) into dedicated classes is the right approach, as the same functionality can be reused in other services (e.g., -_OrdersService, AccountsService_) without duplicating logic or tightly coupling -it to other classes. +_OrdersService, AccountsService_) without duplicating code. --- -The dependencies _could also_ be instantiated by the class that needs them: +Dependencies _could also_ be instantiated by the classes that need them: ```python class ProductsService: @@ -181,9 +179,8 @@ assert isinstance(example.dependency, A) /// admonition | Completely non-intrusive type: tip -Note how `rodi` is completely non-intrusive and does **not** require changing -the source code of the types it handles. This was one of the objectives of the -library. +Notice that Rodi is completely non-intrusive and does **not** require any changes to the +source code of the types it handles. This was one of the library's primary design goals. /// In this example, both `A` and `B` are concrete types. Rodi can resolve concrete @@ -401,7 +398,7 @@ easily become hard to maintain. To simplify the management of dependencies and reduce the complexity of object instantiation, we can leverage a dependency injection framework like `rodi`. -## The Repository pattern example +### The Repository pattern example The three classes described above: `ProductsService`, `ProductsRepository`, and `SQLProductsRepository`, can be wired using `rodi` this way: @@ -456,7 +453,7 @@ print(service.get_all_products()) Some interesting things are happening in this code: - At line _9_, an instance of `rodi.Container` is created. This class is used - to register the types that must be resolved, and resolve those types. + to register the types that must be resolved, and to resolve those types. - It was not necessary to modify the source code of the classes being handled: `rodi` inspects the code of registered types to know how to resolve them. - A factory function is used to define how the instance of `sqlite3.Connection` @@ -464,12 +461,13 @@ Some interesting things are happening in this code: returns an instance of that class, requires a `str`, and resolving base types with `DI` is not a good idea. - The factory has a return type annotation: `rodi` uses that type annotation - as the _key type_ to be resolved using the factory function. -- Because the constructor of the `SQLProductsRepository` class did not include - a type annotation to describe its dependency `db_connection`, an alias is - configured at line _29_, to instruct `rodi` to resolve parameters having - name `db_connection` to obtain an instance of `sqlite3.Connection`. - Alternatively, we could have updated the source code of + as the _key type_ that is resolved using the factory function. Note that a factory + might declare a more **abstract** type than the one it returns (following the DIP + principle). +- Since the constructor of the `SQLProductsRepository` class does not include a type + annotation for its `db_connection` dependency, an alias is configured at line _29_ to + instruct the container to resolve parameters named `db_connection` as instances of + `sqlite3.Connection`. Alternatively, we could have updated the source code of `SQLProductsRepository` to include a type annotation in its constructor. - At line _31_, the **abstract** type `ProductsRepository` is registered, instructing the container to resolve that type with the **concrete** @@ -478,7 +476,7 @@ Some interesting things are happening in this code: using the abstract type as _key_. - At line _32_, the `ProductsService` type is also registered, because this is required to build the graph of dependencies. -- At line _36_, an instance of `ProductsService` it obtained through **DI**. +- At line _36_, an instance of `ProductsService` is obtained through **DI**. Since this is the first time the `Container` needs to resolve a type, it runs code inspections to build the tree of dependencies. These code inspections are executed only once, unless new types are registered in the same @@ -487,6 +485,218 @@ Some interesting things are happening in this code: dependency `ProductsRepository`, used to instantiate the requested `ProductsService`. +## Rodi's use cases + +Rodi is designed to simplify object instantiation and dependency management. It can +inspect constructors (`__init__` methods) and class properties to automatically resolve +dependencies. + +Support for inspecting class properties is intended to reduce code verbosity. Note how +in the example below, it is necessary to write three times 'dependency': + +```python +class A: + ... + + +class B: + def __init__(self, dependency: A): + self.dependency = dependency +``` + +The same classes can be written this way: + +```python +class A: + ... + +class B: + dependency: A +``` + +Rodi would automatically instantiate `B` and populate its `dependency` property +with an instance of `A`. + +```mermaid +graph TD + A[Rodi] --> B[Resolves __init__ methods] + A --> C[Resolves class properties] +``` + +=== "Using constructors" + + ```python {linenums="1", hl_lines="7-8"} + from rodi import Container + + class A: + ... + + class B: + def __init__(self, dependency: A): + self.dependency = dependency + + container = Container() + + container.add_transient(A) + container.add_transient(B) + + example = container.resolve(B) + assert isinstance(example, B) + assert isinstance(example.dependency, A) + ``` + +=== "Using class properties" + + ```python {linenums="1", hl_lines="7"} + from rodi import Container + + class A: + ... + + class B: + dependency: A + + container = Container() + + container.add_transient(A) + container.add_transient(B) + + example = container.resolve(B) + assert isinstance(example, B) + assert isinstance(example.dependency, A) + ``` + +### Container lifetime + +The primary use case of Rodi is to instantiate a single `Container` object, configure it +with all required dependencies at application startup, and maintain it in an immutable +state throughout the application's lifetime. It is anyway possible to work with multiple +containers, and to modify them even after the dependency graph has been built. Modifying +a `Container` after the dependency graph has been built is an anti-pattern and can lead +to unexpected behaviour. More details on this subject are provided in the next page. + +### Sync vs Async + +Rodi is designed for synchronous code. It intentionally does not provide an asynchronous +code API because object constructors should be lightweight and run synchronously. +Supporting asynchronous type resolution would introduce performance overhead due to the +complexity of asynchronous operations, and the extra machinery they require. + +Constructors (`__init__` methods) are typically designed to be lightweight and avoid +CPU intensive blocking operations or performing I/O operations. + +### Type annotations + +Rodi can use both type annotations and naming conventions to build graphs of +dependencies. + +Type annotations is the recommended way to keep the code clean and explicit. + +=== "Using type annotations (recommended)" + + ```python {linenums="1", hl_lines="7"} + from rodi import Container + + class A: + ... + + class B: + dependency: A + + container = Container() + + container.add_transient(A) + container.add_transient(B) + + example = container.resolve(B) + assert isinstance(example, B) + assert isinstance(example.dependency, A) + ``` + +=== "Using naming conventions" + + ```python {linenums="1", hl_lines="7-8 12-13"} + from rodi import Container + + class A: + ... + + class B: + def __init__(self, dependency): # <-- no type annotation + self.dependency = dependency + + container = Container() + + container.add_transient(A) + container.add_alias("dependency", A) # <-- required to resolve + container.add_transient(B) + + example = container.resolve(B) + assert isinstance(example, B) + assert isinstance(example.dependency, A) + ``` + +### Automatic aliases + +Rodi also supports automatic aliases. When a type is registered, the container creates a +set of aliases based on the class name. Consider the following example: + +```python {linenums="1", hl_lines="4 8-9"} +from rodi import Container + + +class CatsRepository: ... + + +class B: + def __init__(self, cats_repository): + self.cats_repository = cats_repository + + +container = Container() + +container.add_transient(CatsRepository) +container.add_transient(B) + +example = container.resolve(B) +assert isinstance(example, B) +assert isinstance(example.cats_repository, CatsRepository) +``` + +Aliases are only used when type annotations are missing. They serve solely as a +*fallback* and always refer to a type that can be resolved. + +This design decision is based on the assumption that classes *usually* have names that +are distinct enough to be unambiguously identified, even across namespaces. + +In the example above, the following set of aliases is created for the registered types: + +```python +{ + 'CatsRepository': {}, + 'catsrepository': {}, + 'cats_repository': {}, + 'B': {}, + 'b': {} +} +``` + +/// admonition | Disabling automatic aliases + type: tip + +Some programmers might dislike the automatic aliasing feature, as it can lead to +unexpected behavior if naming conventions are not followed consistently. To disable this +feature, set the `strict` parameter to `True` when creating the container: + +```python +container = Container(strict=True) +``` +/// + ## Summary -... +This page covered the ABCs of Dependency Injection and Rodi. The general concepts +presented here apply to others DI frameworks as well. + +The next page will start diving into Rodi's details, starting with explaining +[services' lifetime](./services-lifetime.md). diff --git a/rodi/docs/index.md b/rodi/docs/index.md index 6a435d5..d94f712 100644 --- a/rodi/docs/index.md +++ b/rodi/docs/index.md @@ -17,13 +17,11 @@ pip install rodi - A strategy to better organize source code, reduce code repetition, and improve development experience. - Simplified dependency management with automatic resolution and injection of - dependencies, by type annotation in constructors or type properties. -- A fast implementation that performs code inspections only once, rather than + dependencies, by type annotation in constructors or class properties. +- A fast implementation that performs code inspections only when necessary, rather than at each type resolution. - A generic code API that can be used with any kind of Python applications. ## Getting started -To get started with Rodi, read the _Getting Started_ page: - -- [Basics](./getting-started.md) +To get started with Rodi, read the [_Getting Started_](./getting-started.md) guide. diff --git a/rodi/docs/types-lifetime.md b/rodi/docs/types-lifetime.md new file mode 100644 index 0000000..ba2bdf4 --- /dev/null +++ b/rodi/docs/types-lifetime.md @@ -0,0 +1,147 @@ +This page dives into more details, covering the following subjects: + +- [X] Types lifetime. +- [X] Options to register types. +- [X] The `Services` class. + +## Types lifetime + +Rodi supports three kinds of lifetimes: + +- **Singleton** lifetime, for types that must be created only once per container. +- **Transient** lifetime, for types that must be created every time they are + requested. +- **Scoped** lifetime, for types that must be created once per resolution scope + (e.g. once per HTTP web request, once per user interaction). + +The next paragraphs describe each type in detail. + +### Transient lifetime + +Transient lifetime is the most common kind for types registered in Rodi. It means +that a new instance of a class will be created every time it is requested. + +```python +from rodi import Container + +class A: ... + +container = Container() + +container.add_transient(A) + +a1 = container.resolve(A) # a1 is a new instance of A +a2 = container.resolve(A) # a2 is another new instance of A +a1 is not a2 # True +``` + +The `Container` class offers two methods to register types with transient lifetime: + +- **add_transient** to register a _transient_ type by class. +- **add_transient_by_factory** to register a _transient_ type by factory function. + +### Singleton lifetime + +The singleton lifetime is used for types that should be instantiated only once per +container's dependency graph. + +```python +from rodi import Container + +class A: ... + +container = Container() + +container.add_singleton(A) + +a1 = container.resolve(A) # a1 is a new instance of A +a2 = container.resolve(A) # a2 is the same instance of A +a1 is a2 # True +``` + +The `Container` class offers three methods to register types with singleton lifetime: + +- **add_instance** to register a _singleton_ using an instance. +- **add_singleton** to register a _singleton_ type by class. +- **add_singleton_by_factory** to register a _singleton_ type by factory function. + +=== "add_instance" + + ```python {linenums="1", hl_lines="9"} + from rodi import Container + + class Cat: + def __init__(self, name: str): + self.name = name + + container = Container() + + container.add_instance(Cat("Tom")) + + example = container.resolve(Cat) + assert isinstance(example, Cat) + assert example.name == "Tom" + ``` + +=== "add_singleton" + + ```python {linenums="1", hl_lines="8"} + from rodi import Container + + class Cat: + pass + + container = Container() + + container.add_singleton(Cat) + + example = container.resolve(Cat) + assert isinstance(example, Cat) + ``` + +=== "add_singleton_by_factory" + + ```python {linenums="1", hl_lines="9-10"} + from rodi import Container + + class Cat: + def __init__(self, name: str): + self.name = name + + container = Container() + + def cat_factory() -> Cat: + return Cat("Tom") + + container.add_singleton_by_factory(Cat) + + example = container.resolve(Cat) + assert isinstance(example, Cat) + assert example.name == "Tom" + ``` + +/// admonition | Container lifecycle + type: danger + +If you modify the `Container` after the dependency tree has been created, for example +registering a new type after any type has been resolved, all created singletons are +discarded and will be recreated when requested again. Modifying the `Container` during +the lifetime of the application is an anti-pattern, and should be avoided. It also +forces the container to repeat code inspections, reducing performance. + +To avoid exposing the modifiable `container`, use the `container.build_provider()` +method, which returns an instance of `Services` that can only be used to resolve types, +without modifying the container. +/// + +### Container lifetime + +The primary use of Rodi is to create a single instance of the `Container` class, +configure it at application startup, and avoid modifying during the lifetime of the +application. + +However, it is still possible to use multiple instances of the `Container` class and to +modify the configuration of a container even after the depedency tree has been created. +This is to be considered an anti pattern, and has the following drawbacks: + +- If the `Container` is modified after a type has been resolved, the dependency diff --git a/rodi/mkdocs.yml b/rodi/mkdocs.yml index 4d8d3ec..514cc37 100644 --- a/rodi/mkdocs.yml +++ b/rodi/mkdocs.yml @@ -8,7 +8,8 @@ edit_uri: "" nav: - Overview: index.md - - "Getting started": getting-started.md + - Getting started: getting-started.md + - Types lifetime: types-lifetime.md - Neoteroi docs home: "/" theme: @@ -34,7 +35,7 @@ theme: repo: fontawesome/brands/github extra: - header_bg_color: "teal" # "#51003c" + header_bg_color: "teal" # "#51003c" extra_css: - css/neoteroi.css @@ -48,9 +49,9 @@ plugins: - neoteroi.contribs markdown_extensions: -# - markdown.extensions.codehilite: -# linenums: true -# guess_lang: false + # - markdown.extensions.codehilite: + # linenums: true + # guess_lang: false - pymdownx.highlight: use_pygments: true guess_lang: false From 05808b4eacee42e9ae4652824394d05ffd01f157 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Wed, 9 Apr 2025 23:19:38 +0200 Subject: [PATCH 04/13] Update types-lifetime.md --- rodi/docs/types-lifetime.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rodi/docs/types-lifetime.md b/rodi/docs/types-lifetime.md index ba2bdf4..6340148 100644 --- a/rodi/docs/types-lifetime.md +++ b/rodi/docs/types-lifetime.md @@ -101,7 +101,7 @@ The `Container` class offers three methods to register types with singleton life === "add_singleton_by_factory" - ```python {linenums="1", hl_lines="9-10"} + ```python {linenums="1", hl_lines="9-10 12"} from rodi import Container class Cat: From 308ead594a931315651de81c644045e46db89341 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Thu, 10 Apr 2025 22:19:51 +0200 Subject: [PATCH 05/13] WIP --- rodi/docs/async.md | 1 + rodi/docs/dependency-inversion.md | 310 ++++++++++++++++++++++++++++++ rodi/docs/getting-started.md | 16 +- rodi/docs/types-lifetime.md | 290 ++++++++++++++++++++++++---- rodi/mkdocs.yml | 2 + 5 files changed, 574 insertions(+), 45 deletions(-) create mode 100644 rodi/docs/async.md create mode 100644 rodi/docs/dependency-inversion.md diff --git a/rodi/docs/async.md b/rodi/docs/async.md new file mode 100644 index 0000000..262aca3 --- /dev/null +++ b/rodi/docs/async.md @@ -0,0 +1 @@ +Working with async. diff --git a/rodi/docs/dependency-inversion.md b/rodi/docs/dependency-inversion.md new file mode 100644 index 0000000..1cbe7ea --- /dev/null +++ b/rodi/docs/dependency-inversion.md @@ -0,0 +1,310 @@ +This page describes how to apply the _Dependency Inversion Principle_, working with +_abstract_ classes and protocols. + +- [X] Working with interfaces. +- [X] Using abstract classes and protocols. + +## Working with interfaces + +Abstract types are a way to define a common interface for a set of classes. This allows +you to write code that works with any class that implements the interface, without +needing to know the details of the implementation. When registering a type in a +`Container`, you can specify the base _interface_ which is used as _key_ to resolve +_concrete_ types, and the implementation type which is used to create the instance. This +is useful when it is desirable to use the same interface for different implementations, +or when you want to switch to a different implementation in the future without changing +the code that relies on the interface. + +=== "add_transient" + + ```python {linenums="1", hl_lines="9 15 17"} + from abc import ABC, abstractmethod + from rodi import Container + + class MyInterface(ABC): + @abstractmethod + def do_something(self) -> str: + pass + + class MyClass(MyInterface): + def do_something(self) -> str: + return "Hello, world!" + + container = Container() + + container.add_transient(MyInterface, MyClass) + + a1 = container.resolve(MyInterface) + assert isinstance(a1, MyClass) + assert a1.do_something() == "Hello, world!" + ``` + +=== "add_singleton" + + ```python {linenums="1", hl_lines="9 15 17"} + from abc import ABC, abstractmethod + from rodi import Container + + class MyInterface(ABC): + @abstractmethod + def do_something(self) -> str: + pass + + class MyClass(MyInterface): + def do_something(self) -> str: + return "Hello, world!" + + container = Container() + + container.add_singleton(MyInterface, MyClass) + + a1 = container.resolve(MyInterface) + assert isinstance(a1, MyClass) + assert a1.do_something() == "Hello, world!" + ``` + +=== "add_scoped" + + ```python {linenums="1", hl_lines="9 15 17"} + from abc import ABC, abstractmethod + from rodi import Container + + class MyInterface(ABC): + @abstractmethod + def do_something(self) -> str: + pass + + class MyClass(MyInterface): + def do_something(self) -> str: + return "Hello, world!" + + container = Container() + + container.add_scoped(MyInterface, MyClass) + + a1 = container.resolve(MyInterface) + assert isinstance(a1, MyClass) + assert a1.do_something() == "Hello, world!" + ``` + +Using [`ABC` and `abstractmethod`](https://docs.python.org/3/library/abc.html) +is not strictly necessary, but it is recommended for defining interfaces. +This ensures that any class implementing the interface has the required methods. + +If you decide on using a normal class to describe the interface, Rodi requires the +concrete class to be a subclass of the interface. + +Otherwise, you can use a [`Protocol`](https://peps.python.org/pep-0544/) from the +`typing` module to define the interface. In this case, Rodi allows registering a +protocol as the interface and a normal class that does not inherit it (which aligns with +the original purpose of Python's `Protocol`). + +The following examples work: + +=== "Regular class (requires subclassing)" + + ```python {linenums="1", hl_lines="9 16 18"} + from rodi import Container + + + class MyInterface: + def do_something(self) -> str: + pass + + + class MyClass(MyInterface): + def do_something(self) -> str: + return "Hello, world!" + + + container = Container() + + container.add_transient(MyInterface, MyClass) + + a1 = container.resolve(MyInterface) + assert isinstance(a1, MyClass) + assert a1.do_something() == "Hello, world!" + print(a1) + ``` + +=== "Protocol (does not require subclassing)" + + ```python {linenums="1", hl_lines="10 17 19"} + from typing import Protocol + from rodi import Container + + + class MyInterface(Protocol): + def do_something(self) -> str: + pass + + + class MyClass: + def do_something(self) -> str: + return "Hello, world!" + + + container = Container() + + container.add_transient(MyInterface, MyClass) + + a1 = container.resolve(MyInterface) + assert isinstance(a1, MyClass) + assert a1.do_something() == "Hello, world!" + print(a1) + ``` + +Rodi raises an exception if we try registering a normal class as interface, with a +concrete class that does not inherit it. + +/// admonition | Protocols validation + type: warning + +Rodi does **not** validate implementations of Protocols. This means that if you register +a class that does not implement the methods of the Protocol, Rodi will not raise an +exception. Support for Protocols validation might be added in the future, but for now, +you should ensure that the classes you register do implement the methods of the +Protocol. +/// + +--- + +## Using factories + +When using factories to define how types are created, specify the interface by using the +factory's return type annotation. + +```python {linenums="1", hl_lines="13-14 18"} +from abc import ABC, abstractmethod +from rodi import Container + +class MyInterface(ABC): + @abstractmethod + def do_something(self) -> str: + pass + +class MyClass(MyInterface): + def do_something(self) -> str: + return "Hello, world!" + +def my_factory() -> MyInterface: + return MyClass() + +container = Container() + +container.add_transient_by_factory(my_factory) + +a1 = container.resolve(A) +a2 = container.resolve(A) +assert isinstance(a1, A) +assert isinstance(a2, A) +assert a1 is not a2 +``` + +**add_transient_by_factory**, **add_singleton_by_factory**, and **add_scoped_by_factory** +accept a function that returns an instance of the type to register. + +/// admonition | Note about key types. + type: danger + +When working with abstract types, the _interface_ type (or _protocol_) must always be +used as the _key_ type. The implementation type is used to create the instance, but it +is not used as a key to resolve the type. This is according to the [_Dependency +Inversion Principle_](./getting-started.md#dependency-inversion-principle), which states +that high-level modules should not depend on low-level modules, but both should depend +on abstractions. + +This is conceptually wrong: + +```python {linenums="1", hl_lines="10"} +class MyInterface(ABC): + @abstractmethod + def do_something(self) -> str: + pass + +class MyClass(MyInterface): + def do_something(self) -> str: + return "Hello, world!" + +def my_factory() -> MyClass: # <-- No. This is a mistake. + return MyClass() + +container.add_transient_by_factory(my_factory) # <-- MyClass is used as Key. +``` + +/// + +## DI likes custom classes + +Dependency Injection likes custom classes. +Consider this real-life example: + +```python +# domain/emails.py +from abc import ABC, abstractmethod +from dataclasses import dataclass + + +@dataclass +class Email: + recipient: str + sender: str + sender_name: str + subject: str + body: str + cc: list[str] = None + bcc: list[str] = None + + +class EmailHandler(ABC): + @abstractmethod + async def send(self, email: Email) -> None: + pass +``` + +```python {linenums="1", hl_lines="10 12 14"} +# data/sendgrid.py +from domain.emails import Email, EmailHandler +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail + + +class SendGridEmailHandler(EmailHandler): + def __init__(self, api_key: str) -> None: + self.client = SendGridAPIClient(api_key) + + async def send(self, email: Email) -> None: + message = Mail( + from_email=email.sender, + to_emails=email.recipient, + subject=email.subject, + html_content=email.body, + ) + await self.client.send(message) +``` + +There is an issue with the code above. Can you spot it? + + +## Checking if a type is registered + +To check if a type is registered in the container, use the `__contains__` interface: + +```python {linenums="1", hl_lines="11-12"} +from rodi import Container + +class A: ... + +class B: ... + +container = Container() + +container.add_transient(A) + +assert A in container # True +assert B not in container # True +``` + +This can be useful to support alternative ways to register types. For example, tests +code can register a mock type for a class, and the code under test can check if any +interface is already registered in the container, and skip the registration if it is. diff --git a/rodi/docs/getting-started.md b/rodi/docs/getting-started.md index fb8bcb8..b99783e 100644 --- a/rodi/docs/getting-started.md +++ b/rodi/docs/getting-started.md @@ -3,7 +3,7 @@ This page introduces the basics of using Rodi, including: - [X] An overview of dependency injection. -- [X] The use cases `Rodi` is intended for. +- [X] The use cases Rodi is intended for. ## Overview of dependency injection @@ -142,7 +142,7 @@ into the class from the outside. This makes the class more flexible, easier to test, and less dependent on specific implementations. If we consider again the classes `A` and `B` described earlier, they can be -registered and resolved using `rodi` this way: +registered and resolved using Rodi this way: ```python # example1.py @@ -396,12 +396,12 @@ print(service.get_all_products()) As the number of dependencies grow, the code that instantiates objects can easily become hard to maintain. To simplify the management of dependencies and reduce the complexity of object instantiation, we can leverage a dependency -injection framework like `rodi`. +injection framework like Rodi. ### The Repository pattern example The three classes described above: `ProductsService`, `ProductsRepository`, and -`SQLProductsRepository`, can be wired using `rodi` this way: +`SQLProductsRepository`, can be wired using Rodi this way: ```python {linenums="1"} import sqlite3 @@ -455,12 +455,12 @@ Some interesting things are happening in this code: - At line _9_, an instance of `rodi.Container` is created. This class is used to register the types that must be resolved, and to resolve those types. - It was not necessary to modify the source code of the classes being handled: - `rodi` inspects the code of registered types to know how to resolve them. + Rodi inspects the code of registered types to know how to resolve them. - A factory function is used to define how the instance of `sqlite3.Connection` is to be created. This is convenient because the `connect` method, which returns an instance of that class, requires a `str`, and resolving base types with `DI` is not a good idea. -- The factory has a return type annotation: `rodi` uses that type annotation +- The factory has a return type annotation: Rodi uses that type annotation as the _key type_ that is resolved using the factory function. Note that a factory might declare a more **abstract** type than the one it returns (following the DIP principle). @@ -472,7 +472,7 @@ Some interesting things are happening in this code: - At line _31_, the **abstract** type `ProductsRepository` is registered, instructing the container to resolve that type with the **concrete** implementation `SQLProductsRepository`. According to the **DIP** principle, - when registering an abstract type and its implementation, `rodi` requires + when registering an abstract type and its implementation, Rodi requires using the abstract type as _key_. - At line _32_, the `ProductsService` type is also registered, because this is required to build the graph of dependencies. @@ -699,4 +699,4 @@ This page covered the ABCs of Dependency Injection and Rodi. The general concept presented here apply to others DI frameworks as well. The next page will start diving into Rodi's details, starting with explaining -[services' lifetime](./services-lifetime.md). +[types' lifetime](./types-lifetime.md). diff --git a/rodi/docs/types-lifetime.md b/rodi/docs/types-lifetime.md index 6340148..2fd9eb3 100644 --- a/rodi/docs/types-lifetime.md +++ b/rodi/docs/types-lifetime.md @@ -3,6 +3,7 @@ This page dives into more details, covering the following subjects: - [X] Types lifetime. - [X] Options to register types. - [X] The `Services` class. +- [X] The `ContainerProtocol`. ## Types lifetime @@ -18,53 +19,103 @@ The next paragraphs describe each type in detail. ### Transient lifetime -Transient lifetime is the most common kind for types registered in Rodi. It means -that a new instance of a class will be created every time it is requested. +Transient lifetime is the most common kind for types registered in Rodi. It means that a +new instance of a class will be created every time it is requested. The `Container` +class offers three methods to register types with transient lifetime: -```python -from rodi import Container +- **register** to register a _transient_ type by class. +- **add_transient** to register a _transient_ type by class. +- **add_transient_by_factory** to register a _transient_ type by factory function. -class A: ... +=== "register" -container = Container() + ```python {linenums="1", hl_lines="8"} + from rodi import Container -container.add_transient(A) + class A: + ... -a1 = container.resolve(A) # a1 is a new instance of A -a2 = container.resolve(A) # a2 is another new instance of A -a1 is not a2 # True -``` + container = Container() -The `Container` class offers two methods to register types with transient lifetime: + container.register(A) -- **add_transient** to register a _transient_ type by class. -- **add_transient_by_factory** to register a _transient_ type by factory function. + a1 = container.resolve(A) + a2 = container.resolve(A) + assert isinstance(a1, A) + assert isinstance(a2, A) + assert a1 is not a2 + ``` -### Singleton lifetime +=== "add_transient" -The singleton lifetime is used for types that should be instantiated only once per -container's dependency graph. + ```python {linenums="1", hl_lines="8"} + from rodi import Container -```python -from rodi import Container + class A: + ... -class A: ... + container = Container() -container = Container() + container.add_transient(A) + + a1 = container.resolve(A) + a2 = container.resolve(A) + assert isinstance(a1, A) + assert isinstance(a2, A) + assert a1 is not a2 + ``` -container.add_singleton(A) +=== "add_transient_by_factory" -a1 = container.resolve(A) # a1 is a new instance of A -a2 = container.resolve(A) # a2 is the same instance of A -a1 is a2 # True -``` + ```python {linenums="1", hl_lines="6-7 11"} + from rodi import Container -The `Container` class offers three methods to register types with singleton lifetime: + class A: + ... + def a_factory() -> A: + return A() + + container = Container() + + container.add_transient_by_factory(a_factory) + + a1 = container.resolve(A) + a2 = container.resolve(A) + assert isinstance(a1, A) + assert isinstance(a2, A) + assert a1 is not a2 + ``` + +### Singleton lifetime + +The singleton lifetime is used for types that should be instantiated only once per +container's dependency graph. The `Container` class offers three methods to register +types with singleton lifetime: + +- **register** to register a _singleton_ type by class and instance. - **add_instance** to register a _singleton_ using an instance. - **add_singleton** to register a _singleton_ type by class. - **add_singleton_by_factory** to register a _singleton_ type by factory function. +=== "register" + + ```python {linenums="1", hl_lines="7"} + from rodi import Container + + class A: ... + + container = Container() + + container.register(A, instance=A()) + + a1 = container.resolve(A) + a2 = container.resolve(A) + assert isinstance(a1, A) + assert isinstance(a2, A) + assert a1 is not a2 + ``` + === "add_instance" ```python {linenums="1", hl_lines="9"} @@ -127,21 +178,186 @@ If you modify the `Container` after the dependency tree has been created, for ex registering a new type after any type has been resolved, all created singletons are discarded and will be recreated when requested again. Modifying the `Container` during the lifetime of the application is an anti-pattern, and should be avoided. It also -forces the container to repeat code inspections, reducing performance. +forces the container to repeat code inspections, causing a performance fee. -To avoid exposing the modifiable `container`, use the `container.build_provider()` +To avoid exposing the mutable `container`, use the `container.build_provider()` method, which returns an instance of `Services` that can only be used to resolve types, without modifying the container. /// -### Container lifetime +### Scoped lifetime + +The scoped lifetime is used for types that should be instantiated only once per +container's resolution call. The `Container` class offers two methods to register types +with scoped lifetime: + +- **add_scoped** to register a _scoped_ type by class. +- **add_scoped_by_factory** to register a _scoped_ type by factory function. + +=== "add_scoped" + + ```python {linenums="1", hl_lines="7 10 15 19 23 25 29 31"} + from rodi import Container + + class A: + ... + + class B: + context: A + + class C: + context: A + dependency: B -The primary use of Rodi is to create a single instance of the `Container` class, -configure it at application startup, and avoid modifying during the lifetime of the -application. + container = Container() + + container.add_scoped(A) + container.add_scoped(B) + container.add_scoped(C) + + c1 = container.resolve(C) # A is created only once for both B and C + assert isinstance(c1, C) + assert isinstance(c1.dependency, B) + assert isinstance(c1.context, A) + assert c1.context is c1.dependency.context + + c2 = container.resolve(C) + assert isinstance(c2, C) + assert isinstance(c2.dependency, B) + assert isinstance(c2.context, A) + assert c2.context is c2.dependency.context + + assert c1.context is not c2.context + ``` + +=== "add_scoped_by_factory" + + ```python {linenums="1", hl_lines="16-17 22"} + from rodi import Container + + + class A: ... + + + class B: + context: A + + + class C: + context: A + dependency: B + + + def a_factory() -> A: + return A() + + + container = Container() + + container.add_scoped_by_factory(a_factory) + container.add_scoped(B) + container.add_scoped(C) + + c1 = container.resolve(C) # A is created only once for both B and C + assert isinstance(c1, C) + assert isinstance(c1.dependency, B) + assert isinstance(c1.context, A) + assert c1.context is c1.dependency.context + + c2 = container.resolve(C) + assert isinstance(c2, C) + assert isinstance(c2.dependency, B) + assert isinstance(c2.context, A) + assert c2.context is c2.dependency.context + + assert c1.context is not c2.context + ``` + +## The Services class + +The `Container` class in Rodi can be used to register and resolve types, and it is +mutable (new types can be registered at any time). This design decision was driven by +the desire to keep the code API as simple as possible, and to enable the possibility to +replace the Rodi's container with alternative implementations of dependency injection. + +Although the container is mutable, it is generally recommended to use it in the +following way: + +- Register all types in the container during application startup. +- Resolve types at runtime without registering new ones. + +It can be undesirable to expose the mutable `Container` to the application code, as it +can lead to unexpected behavior. For this reason, the `Container` class provides a +method called `build_provider`, which returns a read-only interface that can be used to +resolve types, but not to register new ones. + +```python +from rodi import Container + + +class A: ... + + +container = Container() + +container.add_transient(A) + +provider = container.build_provider() + +a1 = provider.get(A) +a2 = provider.get(A) +assert isinstance(a1, A) +assert isinstance(a2, A) +assert a1 is not a2 +``` + +### The ContainerProtocol + +Rodi defines a protocol for the `Container` class, named `ContainerProtocol`. This +protocol defines a generic interface of the container, which includes methods for +registering and resolving types, as well as checking if a type is configured in the +container. + +The purpose of this protocol is to support replacing Rodi with alternative +implementations of dependency injection in code that requires basic container +functionality. The protocol is defined as follows: + +```python +class ContainerProtocol(Protocol): + """ + Generic interface of DI Container that can register and resolve services, + and tell if a type is configured. + """ + + def register(self, obj_type: Union[Type, str], *args, **kwargs): + """Registers a type in the container, with optional arguments.""" + + def resolve(self, obj_type: Union[Type[T], str], *args, **kwargs) -> T: + """Activates an instance of the given type, with optional arguments.""" + + def __contains__(self, item) -> bool: + """ + Returns a value indicating whether a given type is configured in this container. + """ +``` + +Since some features, like _Service Lifetime_ are specific to Rodi (some alternative +implementations only support _transient_ and _singleton_ lifetimes), the protocol does +not define methods for registering types with different lifetimes. The protocol only +defines unopinionated methods to `register` and `resolve` types, and to check if a type +is configured. + +/// admonition | Interoperability + type: tip + +If you author code that relies on a Dependency Injection container and you want to +support different implementations, you would need to decide on a common interface, or +[_Protocol_](https://peps.python.org/pep-0544/), required by your code. The +`ContainerProtocol` interface was originally thought for this purpose. +/// -However, it is still possible to use multiple instances of the `Container` class and to -modify the configuration of a container even after the depedency tree has been created. -This is to be considered an anti pattern, and has the following drawbacks: +## Next steps -- If the `Container` is modified after a type has been resolved, the dependency +All examples on this page show how to register and resolve _concrete_ classes. +The next page describes how to apply the [_Dependency Inversion Principle_](./dependency-inversion.md), +and how to work with _abstract_ classes and protocols. diff --git a/rodi/mkdocs.yml b/rodi/mkdocs.yml index 514cc37..35f23cc 100644 --- a/rodi/mkdocs.yml +++ b/rodi/mkdocs.yml @@ -10,6 +10,8 @@ nav: - Overview: index.md - Getting started: getting-started.md - Types lifetime: types-lifetime.md + - Dependecy inversion: dependency-inversion.md + - Working with async: async.md - Neoteroi docs home: "/" theme: From d8f09c723da21812fe673239b52874362746b3de Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Fri, 11 Apr 2025 19:51:12 +0200 Subject: [PATCH 06/13] WIP --- rodi/.gitignore | 0 rodi/docs/async.md | 196 +++++++++++++++++- rodi/docs/dependency-inversion.md | 103 +++++---- rodi/docs/getting-started.md | 4 +- ...types-lifetime.md => registering-types.md} | 2 +- rodi/docs/types.md | 36 ++++ rodi/mkdocs.yml | 3 +- 7 files changed, 287 insertions(+), 57 deletions(-) create mode 100644 rodi/.gitignore rename rodi/docs/{types-lifetime.md => registering-types.md} (99%) create mode 100644 rodi/docs/types.md diff --git a/rodi/.gitignore b/rodi/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/rodi/docs/async.md b/rodi/docs/async.md index 262aca3..ea732ff 100644 --- a/rodi/docs/async.md +++ b/rodi/docs/async.md @@ -1 +1,195 @@ -Working with async. +As explained in [_Getting Started_](./getting-started.md), Rodi's objective is to +simplify constructing objects based on constructors and class properties. +Support for async resolution is intentionally out of the scope of the library because +constructing objects should be lightweight. + +This page provides guidelines for working with objects that require asynchronous +initialization. + +## A common example + +A common example of this situation are objects that handle TCP/IP connection pooling, +such as `HTTP` clients and database clients. These objects are usually implemented as +*context managers* in Python because they need to implement connection pooling and +gracefully close TCP connections when disposed. + +Python supports [`asynchronous` context managers](https://peps.python.org/pep-0492/#asynchronous-context-managers-and-async-with) for this kind of scenario. + +Consider the following example, of a `SendGrid` API client to send emails using the +SendGrid API, with asynchronous code and using [`httpx`](https://www.python-httpx.org/async/). + +```python {linenums="1"} +# domain/emails.py +from abc import ABC, abstractmethod +from dataclasses import dataclass + + +# TODO: use Pydantic for the Email object. +@dataclass +class Email: + recipients: list[str] + sender: str + sender_name: str + subject: str + body: str + cc: list[str] = None + bcc: list[str] = None + + +class EmailHandler(ABC): # interface + @abstractmethod + async def send(self, email: Email) -> None: + pass +``` + +```python {linenums="1", hl_lines="24 32"} +# data/apis/sendgrid.py +import os +from dataclasses import dataclass + +import httpx + +from domain.emails import Email, EmailHandler + + +@dataclass +class SendGridClientSettings: + api_key: str + + @classmethod + def from_env(cls): + api_key = os.environ.get("SENDGRID_API_KEY") + if not api_key: + raise ValueError("SENDGRID_API_KEY environment variable is required") + return cls(api_key=api_key) + + +class SendGridClient(EmailHandler): + def __init__( + self, settings: SendGridClientSettings, http_client: httpx.AsyncClient + ): + if not settings.api_key: + raise ValueError("API key is required") + self.http_client = http_client + self.api_key = settings.api_key + + async def send(self, email: Email) -> None: + response = await self.http_client.post( + "https://api.sendgrid.com/v3/mail/send", + headers={ + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + }, + json=self.get_body(email), + ) + # Note: in case of error, inspect response.text + response.raise_for_status() # Raise an error for bad responses + + def get_body(self, email: Email) -> dict: + return { + "personalizations": [ + { + "to": [{"email": recipient} for recipient in email.recipients], + "subject": email.subject, + "cc": [{"email": cc} for cc in email.cc] if email.cc else None, + "bcc": [{"email": bcc} for bcc in email.bcc] if email.bcc else None, + } + ], + "from": {"email": email.sender, "name": email.sender_name}, + "content": [{"type": "text/html", "value": email.body}], + } +``` + +/// details | The official SendGrid Python SDK does not support async. + type: danger + +At the time of this writing, the official SendGrid Python SDK does not support `async`. +Its documentation provides a wrong example for `async` code (see [_issue #988_](https://github.com/sendgrid/sendgrid-python/issues/988)). +The SendGrid REST API is very well documented and comfortable to use! Use a class like +the one shown on this page to send emails using SendGrid in async code. +/// + +The **SendGridClient** depends on an instance of `SendGridClientSettings` (providing a +SendGrid API Key), and on an instance of `httpx.AsyncClient` able to make HTTP requests. + +The code below shows how to register the object that requires asynchronous +initialization and use it across the lifetime of your application. + +```python {linenums="1", hl_lines="12-20 25 40-41 44-46 48"} +# main.py +import asyncio +from contextlib import asynccontextmanager + +import httpx +from rodi import Container + +from data.apis.sendgrid import SendGridClient, SendGridClientSettings +from domain.emails import EmailHandler + + +@asynccontextmanager +async def register_http_client(container: Container): + + async with httpx.AsyncClient() as http_client: + print("HTTP client initialized") + container.add_instance(http_client) + yield + + print("HTTP client disposed") + + +async def application_runtime(container: Container): + # Entry point for what your application does + email_handler = container.resolve(EmailHandler) + assert isinstance(email_handler, SendGridClient) + assert isinstance(email_handler.http_client, httpx.AsyncClient) + + # We can use the HTTP Client during the lifetime of the Application + print("All is good! ✨") + + +def sendgrid_settings_factory() -> SendGridClientSettings: + return SendGridClientSettings.from_env() + + +async def main(): + # Bootstrap code for the application + container = Container() + container.add_singleton_by_factory(sendgrid_settings_factory) + container.add_singleton(EmailHandler, SendGridClient) + + async with register_http_client(container) as http_client: + container.add_instance( + http_client + ) # <-- Configure the HTTP client as singleton + + await application_runtime(container) + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +The above code displays the following: + +```bash +$ SENDGRID_API_KEY="***" python main.py + +HTTP client initialized +All is good! ✨ +HTTP client disposed +``` + +## Considerations + +- It is not Rodi's responsibility to administer the lifecycle of the application. It is + the responsibility of the code that bootstrap the application, to handle objects that + require asynchronous initialization and disposal. +- Python's `asynccontextmanager` is convenient for these scenarios. +- In the example above, the HTTP Client is configured as singleton to benefit from TCP + connection pooling. It would also be possible to configure it as transient or scoped + service, as long as all instances share the same connection pool. In the case of + `httpx`, you can read on this subject here: [Why use a Client?](https://www.python-httpx.org/advanced/clients/#why-use-a-client). +- Dependency Injection likes custom classes to describe _settings_ for types, + because registering simple types (`str`, `int`, `float`, etc.) in the container does + not scale and should be avoided. diff --git a/rodi/docs/dependency-inversion.md b/rodi/docs/dependency-inversion.md index 1cbe7ea..28381ae 100644 --- a/rodi/docs/dependency-inversion.md +++ b/rodi/docs/dependency-inversion.md @@ -204,6 +204,55 @@ assert a1 is not a2 **add_transient_by_factory**, **add_singleton_by_factory**, and **add_scoped_by_factory** accept a function that returns an instance of the type to register. +Valid function signatures include: + +- `def factory():` +- `def factory(context: rodi.ActivationScope):` +- `def factory(context: rodi.ActivationScope, activating_type: type):` + +The context is the current activation scope, and grants access to the set of scoped +services and to the `ServiceProvider` object under construction. +The `activating_type` is the type that is being activated and required resolving the +service. This can be useful in some scenarios, when the returned object must vary +depending on the type that required it. + +```python {linenums="1", hl_lines="13-14 18"} +from rodi import ActivationScope, Container + + +class C: ... + + +class A: ... + + +class B: + friend: A + + +container = Container() + + +def factory(context, activating_type) -> A: + assert isinstance(context, ActivationScope) + assert activating_type is B + + # You can obtain other types using `context.provider.get` + # (if they can be resolved) + c = context.provider.get(C) + assert isinstance(c, C) + + return A() + + +container.add_transient(C) +container.add_transient_by_factory(factory) +container.add_transient(B) + +b = container.resolve(B) +assert isinstance(b.friend, A) +``` + /// admonition | Note about key types. type: danger @@ -234,58 +283,6 @@ container.add_transient_by_factory(my_factory) # <-- MyClass is used as Key. /// -## DI likes custom classes - -Dependency Injection likes custom classes. -Consider this real-life example: - -```python -# domain/emails.py -from abc import ABC, abstractmethod -from dataclasses import dataclass - - -@dataclass -class Email: - recipient: str - sender: str - sender_name: str - subject: str - body: str - cc: list[str] = None - bcc: list[str] = None - - -class EmailHandler(ABC): - @abstractmethod - async def send(self, email: Email) -> None: - pass -``` - -```python {linenums="1", hl_lines="10 12 14"} -# data/sendgrid.py -from domain.emails import Email, EmailHandler -from sendgrid import SendGridAPIClient -from sendgrid.helpers.mail import Mail - - -class SendGridEmailHandler(EmailHandler): - def __init__(self, api_key: str) -> None: - self.client = SendGridAPIClient(api_key) - - async def send(self, email: Email) -> None: - message = Mail( - from_email=email.sender, - to_emails=email.recipient, - subject=email.subject, - html_content=email.body, - ) - await self.client.send(message) -``` - -There is an issue with the code above. Can you spot it? - - ## Checking if a type is registered To check if a type is registered in the container, use the `__contains__` interface: @@ -308,3 +305,5 @@ assert B not in container # True This can be useful to support alternative ways to register types. For example, tests code can register a mock type for a class, and the code under test can check if any interface is already registered in the container, and skip the registration if it is. + +The next page explains how to work with [types and collections](./types.md). diff --git a/rodi/docs/getting-started.md b/rodi/docs/getting-started.md index b99783e..f68947d 100644 --- a/rodi/docs/getting-started.md +++ b/rodi/docs/getting-started.md @@ -698,5 +698,5 @@ container = Container(strict=True) This page covered the ABCs of Dependency Injection and Rodi. The general concepts presented here apply to others DI frameworks as well. -The next page will start diving into Rodi's details, starting with explaining -[types' lifetime](./types-lifetime.md). +The next page will start diving into Rodi's details, starting with explaining how to +register types and [types' lifetime](./types-lifetime.md). diff --git a/rodi/docs/types-lifetime.md b/rodi/docs/registering-types.md similarity index 99% rename from rodi/docs/types-lifetime.md rename to rodi/docs/registering-types.md index 2fd9eb3..2362640 100644 --- a/rodi/docs/types-lifetime.md +++ b/rodi/docs/registering-types.md @@ -360,4 +360,4 @@ support different implementations, you would need to decide on a common interfac All examples on this page show how to register and resolve _concrete_ classes. The next page describes how to apply the [_Dependency Inversion Principle_](./dependency-inversion.md), -and how to work with _abstract_ classes and protocols. +and how to work with _abstract_ classes and protocols, and more details about factories. diff --git a/rodi/docs/types.md b/rodi/docs/types.md new file mode 100644 index 0000000..db951d9 --- /dev/null +++ b/rodi/docs/types.md @@ -0,0 +1,36 @@ +This page covers the following subjects: + +- [X] Recommendations to work with types. +- [X] Support for collections. + +## DI :heart: custom types + +**Dependency Injection** loves custom types. +Consider the following example: + +```python +class Example: + def __init__(self, api_key: str): + if not api_key: + raise ValueError("API key is required") + self.api_key = settings.api_key +``` + +There is a potential issue with the code above. Can you spot it? + +The `Example` class depends on a `str`. We could register a `str` singleton in +our DI container, but it wouldn't make sense. Some other class might require a `str` +dependency, and we would be out of options to resolve then. All types that require a +simple type passed to their constructor are best configured using a _factory_ function. + +We could do: + +```python +def example_factory() -> Example: + return Example(os.environ.get("API_KEY")) +``` + +## Support for collections + +The next page explains how to deal with `async` code and classes that require +[asynchronous initialization](./async.md). diff --git a/rodi/mkdocs.yml b/rodi/mkdocs.yml index 35f23cc..dd8dfa7 100644 --- a/rodi/mkdocs.yml +++ b/rodi/mkdocs.yml @@ -9,8 +9,9 @@ edit_uri: "" nav: - Overview: index.md - Getting started: getting-started.md - - Types lifetime: types-lifetime.md + - Registering types: registering-types.md - Dependecy inversion: dependency-inversion.md + - Types and collections: types.md - Working with async: async.md - Neoteroi docs home: "/" From 87cc2ee45f6b728cdb2f0523e709fce05c0d12e7 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sat, 12 Apr 2025 08:07:41 +0200 Subject: [PATCH 07/13] More information --- rodi/docs/dependency-inversion.md | 125 +++++++++++---------- rodi/docs/getting-started.md | 19 ++-- rodi/docs/registering-types.md | 173 +++++++++++++++++++++++++++--- rodi/docs/types.md | 36 ------- rodi/mkdocs.yml | 1 - 5 files changed, 232 insertions(+), 122 deletions(-) delete mode 100644 rodi/docs/types.md diff --git a/rodi/docs/dependency-inversion.md b/rodi/docs/dependency-inversion.md index 28381ae..03f6fe0 100644 --- a/rodi/docs/dependency-inversion.md +++ b/rodi/docs/dependency-inversion.md @@ -1,8 +1,9 @@ -This page describes how to apply the _Dependency Inversion Principle_, working with -_abstract_ classes and protocols. +This page describes how to apply the [_Dependency Inversion Principle_](./getting-started.md#dependency-inversion-principle), working with _abstract_ classes, protocols, +and generics. - [X] Working with interfaces. - [X] Using abstract classes and protocols. +- [X] Working with generics. ## Working with interfaces @@ -169,10 +170,10 @@ Protocol. --- -## Using factories +## Note about factories -When using factories to define how types are created, specify the interface by using the -factory's return type annotation. +When using factories to define how abstract types are created, ensure the +factory's return type annotation specifies the _interface_. ```python {linenums="1", hl_lines="13-14 18"} from abc import ABC, abstractmethod @@ -201,58 +202,6 @@ assert isinstance(a2, A) assert a1 is not a2 ``` -**add_transient_by_factory**, **add_singleton_by_factory**, and **add_scoped_by_factory** -accept a function that returns an instance of the type to register. - -Valid function signatures include: - -- `def factory():` -- `def factory(context: rodi.ActivationScope):` -- `def factory(context: rodi.ActivationScope, activating_type: type):` - -The context is the current activation scope, and grants access to the set of scoped -services and to the `ServiceProvider` object under construction. -The `activating_type` is the type that is being activated and required resolving the -service. This can be useful in some scenarios, when the returned object must vary -depending on the type that required it. - -```python {linenums="1", hl_lines="13-14 18"} -from rodi import ActivationScope, Container - - -class C: ... - - -class A: ... - - -class B: - friend: A - - -container = Container() - - -def factory(context, activating_type) -> A: - assert isinstance(context, ActivationScope) - assert activating_type is B - - # You can obtain other types using `context.provider.get` - # (if they can be resolved) - c = context.provider.get(C) - assert isinstance(c, C) - - return A() - - -container.add_transient(C) -container.add_transient_by_factory(factory) -container.add_transient(B) - -b = container.resolve(B) -assert isinstance(b.friend, A) -``` - /// admonition | Note about key types. type: danger @@ -283,6 +232,68 @@ container.add_transient_by_factory(my_factory) # <-- MyClass is used as Key. /// +## Working with generics + +Generic types are supported. + +```python {linenums="1", hl_lines="1 6 9 29 34 40-41 44-45"} +from typing import Generic, TypeVar + +from rodi import Container + + +T = TypeVar("T") + + +class LoggedVar(Generic[T]): + def __init__(self, value: T, name: str): + self.name = name + self.value = value + + def set(self, new: T): + self.log("Set " + repr(self.value)) + self.value = new + + def get(self) -> T: + self.log("Get " + repr(self.value)) + return self.value + + def log(self, message: str): + print(self.name, message) + + +container = Container() + + +class A(LoggedVar[int]): + def __init__(self): + super().__init__(10, "example") + + +class B(LoggedVar[str]): + def __init__(self): + super().__init__("Foo", "example") + + +class C: + a: LoggedVar[int] + b: LoggedVar[str] + + +container.add_scoped(LoggedVar[int], A) +container.add_scoped(LoggedVar[str], B) +container.add_scoped(C) + +instance = container.resolve(C) + +assert isinstance(instance.a, A) +assert isinstance(instance.b, B) +``` + +As described above, use the *most* abstract class as the key to resolve more +*concrete* types, in accordance with the Dependency Inversion Principle (DIP). Generics are the **most** abstract +type, so use them as keys like in the example above at lines _44-45_. + ## Checking if a type is registered To check if a type is registered in the container, use the `__contains__` interface: diff --git a/rodi/docs/getting-started.md b/rodi/docs/getting-started.md index f68947d..70d16e5 100644 --- a/rodi/docs/getting-started.md +++ b/rodi/docs/getting-started.md @@ -116,14 +116,8 @@ as it leads to: because tests cannot easily isolate or mock dependencies. Each test might inadvertently affect or be affected by the global state, leading to flaky tests. -- **Lack of Flexibility**: If you need to use different implementations of a - dependency (e.g., for testing, staging, or production environments), global - variables make it more cumbersome to switch implementations dynamically. -Now that we have arguments for _not_ instantiating dependencies inside the -classes that need them and for not instantiating them as global variables, -let's see how dependency injection can help addressing the problems listed -above. +Dependency injection can help addressing the problems listed above. ### Inversion of Control @@ -403,7 +397,7 @@ injection framework like Rodi. The three classes described above: `ProductsService`, `ProductsRepository`, and `SQLProductsRepository`, can be wired using Rodi this way: -```python {linenums="1"} +```python {linenums="1", hl_lines="9 12 28-29 31 36"} import sqlite3 from rodi import Container @@ -487,7 +481,7 @@ Some interesting things are happening in this code: ## Rodi's use cases -Rodi is designed to simplify object instantiation and dependency management. It can +Rodi is designed to simplify objects instantiation and dependency management. It can inspect constructors (`__init__` methods) and class properties to automatically resolve dependencies. @@ -601,7 +595,8 @@ Type annotations is the recommended way to keep the code clean and explicit. ... class B: - dependency: A + def __init__(self, dependency: A): # <-- with type annotation + self.dependency = dependency container = Container() @@ -638,7 +633,7 @@ Type annotations is the recommended way to keep the code clean and explicit. ### Automatic aliases -Rodi also supports automatic aliases. When a type is registered, the container creates a +Rodi supports automatic aliases. When a type is registered, the container creates a set of aliases based on the class name. Consider the following example: ```python {linenums="1", hl_lines="4 8-9"} @@ -699,4 +694,4 @@ This page covered the ABCs of Dependency Injection and Rodi. The general concept presented here apply to others DI frameworks as well. The next page will start diving into Rodi's details, starting with explaining how to -register types and [types' lifetime](./types-lifetime.md). +[register types](./registering-types.md). diff --git a/rodi/docs/registering-types.md b/rodi/docs/registering-types.md index 2362640..4bdcbab 100644 --- a/rodi/docs/registering-types.md +++ b/rodi/docs/registering-types.md @@ -2,6 +2,10 @@ This page dives into more details, covering the following subjects: - [X] Types lifetime. - [X] Options to register types. +- [X] Using factories. +- [X] Working with simple types. +- [X] Support for collections. +- [X] Working with generic types. - [X] The `Services` class. - [X] The `ContainerProtocol`. @@ -181,8 +185,10 @@ the lifetime of the application is an anti-pattern, and should be avoided. It al forces the container to repeat code inspections, causing a performance fee. To avoid exposing the mutable `container`, use the `container.build_provider()` -method, which returns an instance of `Services` that can only be used to resolve types, -without modifying the container. +method, which returns an instance of `Services` that can only be used to +resolve types, without modifying the tree graph. The `Services` class still +offers a `set` method, which can only be used to add new singletons to the +set of types that can be instantiated. /// ### Scoped lifetime @@ -273,12 +279,144 @@ with scoped lifetime: assert c1.context is not c2.context ``` + +## Using factories + +**add_transient_by_factory**, **add_singleton_by_factory**, and **add_scoped_by_factory** +accept a function that returns an instance of the type to register. + +Valid function signatures include: + +- `def factory():` +- `def factory(context: rodi.ActivationScope):` +- `def factory(context: rodi.ActivationScope, activating_type: type):` + +The context is the current activation scope, and grants access to the set of +scoped services and to the `ServiceProvider` object under construction. The +`activating_type` is the type that is being activated and required resolving +the service. This can be useful in some scenarios, when the returned object +must vary depending on the type that required it. + +```python {linenums="1", hl_lines="15-16"} +from rodi import ActivationScope, Container + + +class A: ... + +class B: + friend: A + +class C: ... + +container = Container() + + +def a_factory(context, activating_type) -> A: + assert isinstance(context, ActivationScope) + assert activating_type is B + + # You can obtain other types using `context.provider.get` + # (if they can be resolved) + c = context.provider.get(C) + assert isinstance(c, C) + + return A() + + +container.add_transient_by_factory(a_factory) +container.add_transient(B) +container.add_transient(C) + +b = container.resolve(B) +assert isinstance(b.friend, A) +``` + +## Working with simple types + +**Dependency Injection** loves custom types. Consider the following example: + +```python +class Example: + def __init__(self, api_key: str): + if not api_key: + raise ValueError("API key is required") + self.api_key = settings.api_key +``` + +The `Example` class depends on a `str`. We could register a `str` singleton in +our DI container, but it wouldn't make sense. Some other class might require a +`str` dependency, and we would be out of options to resolve them. All types +that require a simple type passed to their constructor are best configured +using a _factory_ function. + +```python +def example_factory() -> Example: + return Example(os.environ.get("API_KEY")) +``` + +In many cases, it is advisable to define custom types to group settings +consisting of simple types into dedicated classes. + +For example: + +```python {linenums="1", hl_lines="2-3 7"} +@dataclass +class SendGridClientSettings: + api_key: str + + +class SendGridClient(EmailHandler): + settings: SendGridClientSettings + http_client: httpx.AsyncClient +``` + +This approach has the following benefits: + +- A factory can be used to obtain the settings class. +- The more complex type can be resolved using less verbose methods that inspect + its constructor or class properties. + +## Support for collections + +Rodi supports registering and resolving collections. + +```python {linenums="1", hl_lines="11-12"} +from rodi import Container + + +class A: ... + + +class B: + friends: list[A] + + +def friends_factory() -> list[A]: + return [A(), A()] + + +container = Container() + +container.add_transient_by_factory(friends_factory) +container.add_transient(B) + +b = container.resolve(B) +print(b.friends) +assert isinstance(b.friends, list) +assert isinstance(b.friends[0], A) +assert isinstance(b.friends[1], A) +``` + +Other containers such as `dict`, `set`, `Iterable`, `Mapping`, `Sequence`, +`Tuple` are also supported. + ## The Services class -The `Container` class in Rodi can be used to register and resolve types, and it is -mutable (new types can be registered at any time). This design decision was driven by -the desire to keep the code API as simple as possible, and to enable the possibility to -replace the Rodi's container with alternative implementations of dependency injection. +The `Container` class in Rodi can be used to register and resolve types, and it +is mutable (new types can be registered at any time). This design decision was +driven by the desire to keep the code API as simple as possible, and to enable +the possibility to replace the Rodi's container with alternative +implementations of dependency injection. Although the container is mutable, it is generally recommended to use it in the following way: @@ -286,10 +424,12 @@ following way: - Register all types in the container during application startup. - Resolve types at runtime without registering new ones. -It can be undesirable to expose the mutable `Container` to the application code, as it -can lead to unexpected behavior. For this reason, the `Container` class provides a -method called `build_provider`, which returns a read-only interface that can be used to -resolve types, but not to register new ones. +It can be undesirable to expose the mutable `Container` to the application +code, as it can lead to unexpected behavior. For this reason, the `Container` +class provides a method called `build_provider`, which returns a read-only +interface that can be used to resolve types, but not to register new ones +(with the exception of the `set` method, which allows adding new singletons +without altering the existing dependency tree). ```python from rodi import Container @@ -313,10 +453,10 @@ assert a1 is not a2 ### The ContainerProtocol -Rodi defines a protocol for the `Container` class, named `ContainerProtocol`. This -protocol defines a generic interface of the container, which includes methods for -registering and resolving types, as well as checking if a type is configured in the -container. +Rodi defines a protocol for the `Container` class, named `ContainerProtocol`. +This protocol defines a generic interface of the container, which includes +methods for registering and resolving types, as well as checking if a type is +configured in the container. The purpose of this protocol is to support replacing Rodi with alternative implementations of dependency injection in code that requires basic container @@ -337,7 +477,8 @@ class ContainerProtocol(Protocol): def __contains__(self, item) -> bool: """ - Returns a value indicating whether a given type is configured in this container. + Returns a value indicating whether a given type is configured in this + container. """ ``` @@ -360,4 +501,4 @@ support different implementations, you would need to decide on a common interfac All examples on this page show how to register and resolve _concrete_ classes. The next page describes how to apply the [_Dependency Inversion Principle_](./dependency-inversion.md), -and how to work with _abstract_ classes and protocols, and more details about factories. +how to work with _abstract_ classes, protocols, and generics. diff --git a/rodi/docs/types.md b/rodi/docs/types.md deleted file mode 100644 index db951d9..0000000 --- a/rodi/docs/types.md +++ /dev/null @@ -1,36 +0,0 @@ -This page covers the following subjects: - -- [X] Recommendations to work with types. -- [X] Support for collections. - -## DI :heart: custom types - -**Dependency Injection** loves custom types. -Consider the following example: - -```python -class Example: - def __init__(self, api_key: str): - if not api_key: - raise ValueError("API key is required") - self.api_key = settings.api_key -``` - -There is a potential issue with the code above. Can you spot it? - -The `Example` class depends on a `str`. We could register a `str` singleton in -our DI container, but it wouldn't make sense. Some other class might require a `str` -dependency, and we would be out of options to resolve then. All types that require a -simple type passed to their constructor are best configured using a _factory_ function. - -We could do: - -```python -def example_factory() -> Example: - return Example(os.environ.get("API_KEY")) -``` - -## Support for collections - -The next page explains how to deal with `async` code and classes that require -[asynchronous initialization](./async.md). diff --git a/rodi/mkdocs.yml b/rodi/mkdocs.yml index dd8dfa7..31f6cd9 100644 --- a/rodi/mkdocs.yml +++ b/rodi/mkdocs.yml @@ -11,7 +11,6 @@ nav: - Getting started: getting-started.md - Registering types: registering-types.md - Dependecy inversion: dependency-inversion.md - - Types and collections: types.md - Working with async: async.md - Neoteroi docs home: "/" From 2e2028e07dd017427b76c4680f1deed32a1f5b56 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sat, 12 Apr 2025 10:28:43 +0200 Subject: [PATCH 08/13] Complete first Rodi's documentation --- blacksheep/docs/dependency-injection.md | 3 + home/docs/img/rodi.png | Bin 0 -> 61528 bytes home/docs/index.md | 6 ++ home/mkdocs.yml | 1 + pack.sh | 1 + rodi/docs/async.md | 2 + rodi/docs/container-protocol.md | 1 - rodi/docs/context-managers.md | 113 ++++++++++++++++++++++++ rodi/docs/css/extra.css | 4 + rodi/docs/dependency-inversion.md | 4 +- rodi/docs/errors.md | 94 ++++++++++++++++++++ rodi/docs/getting-started.md | 4 +- rodi/docs/index.md | 1 + rodi/docs/registering-types.md | 4 +- rodi/docs/union-types.md | 106 ++++++++++++++++++++++ rodi/mkdocs.yml | 3 + 16 files changed, 340 insertions(+), 7 deletions(-) create mode 100644 home/docs/img/rodi.png delete mode 100644 rodi/docs/container-protocol.md create mode 100644 rodi/docs/context-managers.md create mode 100644 rodi/docs/errors.md create mode 100644 rodi/docs/union-types.md diff --git a/blacksheep/docs/dependency-injection.md b/blacksheep/docs/dependency-injection.md index 4a2dd84..d50dfe8 100644 --- a/blacksheep/docs/dependency-injection.md +++ b/blacksheep/docs/dependency-injection.md @@ -11,6 +11,9 @@ This page describes: - [X] Examples of dependency injection. - [X] How to use alternatives to `rodi`. +!!! info "Rodi documentation" + Detailed documentation for Rodi can be found at: [_Rodi_](/rodi/). + ## Introduction The `Application` object exposes a `services` property that can be used to diff --git a/home/docs/img/rodi.png b/home/docs/img/rodi.png new file mode 100644 index 0000000000000000000000000000000000000000..e4b16afac9f8b3cbd16ce5d0ceebcdc72a3d889f GIT binary patch literal 61528 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 z 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

oR(2zzqwS8f|~X zC)Mvu>@dA@B{;T{zrG%e1Dp?{<`K^+WRj>7Hh*3b>(wjc;QQVN89<66yLXiDp4#$rXU`RX zf@DDNJ@7~TI7Yqj6QGDRfk)So}x!RKQ*6vXd!Nv`J z;3#rSu3DQj)Cvx^8baSWpy8;>-%%f#bG4P-uKXG&hxcl!{X*B6X5;Yl8AZF(;)I2W z$)26I6f`?{j&k1Z?+*uWiSO1d5+e)#R+@ zNVAV)Un}^=x_dAN1Ad6VvOALL8e#-+kMXkxv|3|dpUVFBYN$Ra=oh&iUI#Z%h=9Fp zS?9)llio?rZai)6olC5Sx3edl9i!hd;^-33X?`$((I9h(UKF$yS{AjtwoVf^LT?Gw zyHbo1eeMIgtA#hd532>E3KKu`_Ft-?HQ&sc=;^H2UKF-qZmeY4j*Sx9pzVFoxnH(A z;}xc~WU$(mQ;&o9YZIsA-Q@x9q2zj`(@cjJWL>g9_F3Z+Ojf7QKUzF`Qf{Q8Ll}&9 zE~~C1_|*dN+;o8_sm2R7KMm|fnSj^s{&Hn0;Pt~tR1Y+s${We~d=at9WZqwRDP>vg zDQ6utWZcR(ezD4NHyEHHGVX{(qj+Sm?#-Kf;Nm6q7w*lO1&eAX!~tDQ^4O=OR0HTB zQe4H=p@n|vVzMR_eP8=`7v>)ez^fyCP#8xU9T|D>n&=;&Prd%Od5DdCfBuxL8RWas zZZBu2mho|qn*{lUP^s5-_42R**WoXi?l($4*`JYBQDm{s4pW(jCDRU@Fvlh>3e2GN zvN$0y=k-;Q@JB%fmy+TP3JQvdj5IV*y*vYH+n)9;`oKvNCh#U+&TKVT&@_(8$Y*&O za1m{YqN+Y&-eZgwg2Llo=jj4(ZTrEJeN-KfupfA|kMyx)<-D0V8Q^psnGY|4S;W+Y z)5gJmGE2^XbM`s7thVjg&Of;d&0xA~VxO!{?Pe|KYW?>3jHsb;YA8t@d|LTqRljO) z1%OX0aN|BFh_>*LJz-F@e$$s56_{_Ma*Cj!RI_4NY(-yMN&`a~3ASl$X^7wb+GwAa zF8TeRV@Ycv0k!Jh+QL#8oNIr#yZOy`x^0c8AMzIvwdfGrRqQkC2|YO%H#Y=D9`##k zQCckEr&h>8J_;BhLLl)VB9v75j_kiKg2B14-_c9ukH62@v6rL2OXH5YCluPLDwg4 zQFuD!XP&&Nt_G+l-rR7G$Hp%pux^pL=Z#PTa*KO<-!-kJlu{!>Cm|LC)V(`CKAygT zTJ-2C%YlxOlJzIjDH~e8n_yIA_4c6a?=tVc_vtt}fw-Nd6L&n61+Xyo#a8R!oa~U- zD-<9Szs-4gQFoB!*MBSYQ0VsuP!W%act-1#5n& z`;PM2%R3s|P!G=N@B!41F3jEYsV(_Ns6l#{)CIx6(sc9nRd#n2{?1Z=b@KlPnQ%_WRRmL}DK!8L; ziW85gB!#hlVtdd<`oSa4zV7agB6sDR<=3}T8|_vat*&en)E+cZP&lVz;G~?Cfv$LM zDc3iL#VemTHRUTBaV}P-t=|bbr5gVZVK%?hyjX None: + print("A created") + self.initialized = False + self.disposed = False + + def __enter__(self) -> "A": + print("A initialized") + self.initialized = True + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.disposed = True + print("A destroyed") + + +class B: + def __init__(self, dependency: A) -> None: + self.dependency = dependency + + def do_work(self): + with self.dependency: + print("Do work") + + +container = Container() + +container.register(A) +container.register(B) + +b = container.resolve(B) + +# b.dependency is instantiated and provided as is, it is not entered +# automatically +assert b.dependency.initialized is False +assert b.dependency.disposed is False + +b.do_work() +assert b.dependency.initialized is True +assert b.dependency.disposed is True +``` + +/// admonition | Rodi does not enter and exit contexts. + type: into + +There is no way to unambiguously know the intentions of the developer: +should a context be entered automatically and disposed automatically? +/// + +## Async context managers + +As described above for context managers, Rodi does not handle async context +managers in any special way either. + +```python {linenums="1", hl_lines="6 12 17 26-27 41"} +import asyncio + +from rodi import Container + + +class A: + def __init__(self) -> None: + print("A created") + self.initialized = False + self.disposed = False + + async def __aenter__(self) -> "A": + print("A initialized") + self.initialized = True + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + self.disposed = True + print("A destroyed") + + +class B: + def __init__(self, dependency: A) -> None: + self.dependency = dependency + + async def do_work(self): + async with self.dependency: + print("Do work") + + +container = Container() + +container.register(A) +container.register(B) + +b = container.resolve(B) +assert b.dependency.initialized is False +assert b.dependency.disposed is False + + +asyncio.run(b.do_work()) +assert b.dependency.initialized is True +assert b.dependency.disposed is True +``` + +The next page describes support for [_Union types_](./union-types.md) in Rodi. diff --git a/rodi/docs/css/extra.css b/rodi/docs/css/extra.css index 8720e62..d7efc58 100644 --- a/rodi/docs/css/extra.css +++ b/rodi/docs/css/extra.css @@ -10,6 +10,10 @@ html { overflow-y: scroll; } +.md-typeset__table tr td code { + white-space: nowrap; +} + @media screen and (min-width: 1000px) { html.fullscreen { .md-grid { diff --git a/rodi/docs/dependency-inversion.md b/rodi/docs/dependency-inversion.md index 03f6fe0..943cfce 100644 --- a/rodi/docs/dependency-inversion.md +++ b/rodi/docs/dependency-inversion.md @@ -158,7 +158,7 @@ The following examples work: Rodi raises an exception if we try registering a normal class as interface, with a concrete class that does not inherit it. -/// admonition | Protocols validation +/// admonition | Protocols validation. type: warning Rodi does **not** validate implementations of Protocols. This means that if you register @@ -317,4 +317,4 @@ This can be useful to support alternative ways to register types. For example, t code can register a mock type for a class, and the code under test can check if any interface is already registered in the container, and skip the registration if it is. -The next page explains how to work with [types and collections](./types.md). +The next page explains how to work with [async](./async.md). diff --git a/rodi/docs/errors.md b/rodi/docs/errors.md new file mode 100644 index 0000000..7021c17 --- /dev/null +++ b/rodi/docs/errors.md @@ -0,0 +1,94 @@ +This page describes errors and custom exceptions raised by Rodi. + +## Errors + +| Name | Description | +| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `CannotResolveTypeException` | This error is raised when a type cannot be resolved because it was not registered in the container. | +| `CircularDependencyException` | This error is raised when a circular dependency is detected and cannot be resolved. | +| `FactoryMissingContextException` | This error is raised when a factory function does not have `_locals`. This generally happens only if classes are defined inside functions and use locals (not a common case). | +| `MissingTypeException` | This error is raised when a factory function does not specify its return type annotation, and the user does not specify the type it returns. | +| `OverridingServiceException` | This error is raised when the user tries to override a type that is already registered in the container. | + +## Cannot resolve type + +```python {linenums="1", hl_lines="11"} +from rodi import Container + + +class A: ... + +class B: + dependency: A + + +container = Container() +container.register(B) + +container.resolve(B) # <-- raises exception 💥 +``` + +`rodi.CannotResolveParameterException: Unable to resolve parameter 'dependency' when resolving 'B'` + +All dependencies must be explicitly registered in the container. To resolve the +error, register the missing type: + +```python {linenums="1", hl_lines="11-12"} +from rodi import Container + + +class A: ... + +class B: + dependency: A + + +container = Container() +container.register(A) +container.register(B) + +container.resolve(B) +``` + +## The chicken and egg problem :chicken: :egg: + +The following classes have a circular dependency: + +```python +class Chicken: + egg: "Egg" + + +class Egg: + chicken: Chicken +``` + +If we try to have Rodi resolve them automatically, we get an error: + +```python {linenums="1", hl_lines="4-5 8-9 17"} +from rodi import Container + + +class Chicken: + egg: "Egg" + + +class Egg: + chicken: Chicken + + +container = Container() +container.register(Chicken) +container.register(Egg) + +# The following line raises an exception: +chicken = container.resolve(Chicken) # 💥 +``` + +The raised error is: + +` raise CircularDependencyException(chain[0], concrete_type) +rodi.CircularDependencyException: A circular dependency was detected for the service of type 'Chicken' for 'Egg'`. + +Rodi cannot infer automatically which type should be instantiated first: +_Chicken_ or _Egg_? diff --git a/rodi/docs/getting-started.md b/rodi/docs/getting-started.md index 70d16e5..9e4b0c8 100644 --- a/rodi/docs/getting-started.md +++ b/rodi/docs/getting-started.md @@ -170,7 +170,7 @@ assert isinstance(example, B) assert isinstance(example.dependency, A) ``` -/// admonition | Completely non-intrusive +/// admonition | Completely non-intrusive. type: tip Notice that Rodi is completely non-intrusive and does **not** require any changes to the @@ -676,7 +676,7 @@ In the example above, the following set of aliases is created for the registered } ``` -/// admonition | Disabling automatic aliases +/// admonition | Disabling automatic aliases. type: tip Some programmers might dislike the automatic aliasing feature, as it can lead to diff --git a/rodi/docs/index.md b/rodi/docs/index.md index d94f712..5cf2672 100644 --- a/rodi/docs/index.md +++ b/rodi/docs/index.md @@ -25,3 +25,4 @@ pip install rodi ## Getting started To get started with Rodi, read the [_Getting Started_](./getting-started.md) guide. +To dive straight into instructions on using DI with Rodi, see [_Registering types_](./registering-types.md). diff --git a/rodi/docs/registering-types.md b/rodi/docs/registering-types.md index 4bdcbab..f2e1c6d 100644 --- a/rodi/docs/registering-types.md +++ b/rodi/docs/registering-types.md @@ -175,7 +175,7 @@ types with singleton lifetime: assert example.name == "Tom" ``` -/// admonition | Container lifecycle +/// admonition | Container lifecycle. type: danger If you modify the `Container` after the dependency tree has been created, for example @@ -488,7 +488,7 @@ not define methods for registering types with different lifetimes. The protocol defines unopinionated methods to `register` and `resolve` types, and to check if a type is configured. -/// admonition | Interoperability +/// admonition | Interoperability. type: tip If you author code that relies on a Dependency Injection container and you want to diff --git a/rodi/docs/union-types.md b/rodi/docs/union-types.md new file mode 100644 index 0000000..dc70a41 --- /dev/null +++ b/rodi/docs/union-types.md @@ -0,0 +1,106 @@ +This page describes support for _Union_ types in Rodi. + +- [X] Optional dependencies. +- [X] Union types dependencies. + +## Optional dependencies + +It is uncommon for types resolved with dependency injection to have optional +dependencies, however this scenario is supported by Rodi. + +```python {linenums="1", hl_lines="8 12 15"} +from rodi import Container + + +class A: ... + + +class B: + dependency: A | None + + +container = Container() +container.register(A | None, A) +container.register(B) + +b = container.resolve(B) +assert isinstance(b.dependency, A) +``` + +/// admonition | Optional types keys. + type: warning + +Beware that if you specify a _T_ dependency as _optional_, the _key_ type used to +resolve the dependency becomes the _T | None_ and it is not just _T_. +/// + +A factory function can be used to define logic that determines how the +dependency must be resolved: + +```python {linenums="1", hl_lines="8 12 15"} +from rodi import Container + + +class A: ... + + +class B: + dependency: A | None + + +def a_factory() -> A | None: + # TODO: implement logic that determines what to return + return None + + +container = Container() +container.add_transient_by_factory(a_factory) +container.register(B) + +b = container.resolve(B) +assert b.dependency is None +``` + +## Union dependencies + +Union types are also supported: + +```python {linenums="1", hl_lines="11 14 20 23-24"} +from rodi import Container + + +class A: ... + + +class B: ... + + +class C: + dependency: A | B + + +def ab_factory() -> A | B: + # TODO: implement logic that determines what to return + return A() + + +container = Container() +container.add_transient_by_factory(ab_factory) +container.register(C) + +c = container.resolve(C) +assert isinstance(c.dependency, A) +``` + +/// admonition | Union types keys. + type: warning + +Beware that if you specify a union dependency such as _T | U_ the _key_ type +used to resolve the dependency is _T | U_. Trying to use _T_ or _U_ +singularly causes a _`CannotResolveTypeException`_. +/// + + +--- + +The next page provides an overview of [errors](./errors.md) raised by Rodi. diff --git a/rodi/mkdocs.yml b/rodi/mkdocs.yml index 31f6cd9..52abdb3 100644 --- a/rodi/mkdocs.yml +++ b/rodi/mkdocs.yml @@ -12,6 +12,9 @@ nav: - Registering types: registering-types.md - Dependecy inversion: dependency-inversion.md - Working with async: async.md + - Context managers: context-managers.md + - Union types: union-types.md + - Errors: errors.md - Neoteroi docs home: "/" theme: From 5ff21e88d31cf179e1e80b2913f441fec5d0aa78 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sat, 12 Apr 2025 10:31:28 +0200 Subject: [PATCH 09/13] Update dependency-injection.md --- blacksheep/docs/dependency-injection.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blacksheep/docs/dependency-injection.md b/blacksheep/docs/dependency-injection.md index d50dfe8..bfadd09 100644 --- a/blacksheep/docs/dependency-injection.md +++ b/blacksheep/docs/dependency-injection.md @@ -11,7 +11,7 @@ This page describes: - [X] Examples of dependency injection. - [X] How to use alternatives to `rodi`. -!!! info "Rodi documentation" +!!! info "Rodi's documentation" Detailed documentation for Rodi can be found at: [_Rodi_](/rodi/). ## Introduction From 68d7bb72a44d239f99499fe05a8303ede440f9c1 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sat, 12 Apr 2025 10:41:36 +0200 Subject: [PATCH 10/13] Update getting-started.md --- rodi/docs/getting-started.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rodi/docs/getting-started.md b/rodi/docs/getting-started.md index 9e4b0c8..e598bff 100644 --- a/rodi/docs/getting-started.md +++ b/rodi/docs/getting-started.md @@ -195,7 +195,7 @@ Principle. Consider the following example, of `ProductsService`, `ProductsRepository`, and `SQLProductsRepository`. -```python +```python {linenums="1", hl_lines="14-15 34-35"} # domain/products.py from abc import ABC, abstractmethod from dataclasses import dataclass @@ -253,7 +253,7 @@ class ProductsService: ``` -```python +```python {linenums="1", hl_lines="5-6"} # data/sql/products.py from domain.products import Product, ProductsRepository @@ -353,7 +353,7 @@ The benefits of DIP are: To better understand the concept, consider the following example that shows how those classes can be imported and instantiated: -```python +```python {linenums="1", hl_lines="19-20 22-23"} import sqlite3 from data.sql.products import SQLProductsRepository From 76476752ae1d5fce945f02a8dd9f5a3a4caa89b1 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sat, 12 Apr 2025 10:46:06 +0200 Subject: [PATCH 11/13] Remove unused files --- rodi/.gitignore | 0 rodi/Makefile | 24 ------------------------ 2 files changed, 24 deletions(-) delete mode 100644 rodi/.gitignore delete mode 100644 rodi/Makefile diff --git a/rodi/.gitignore b/rodi/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/rodi/Makefile b/rodi/Makefile deleted file mode 100644 index 467ec68..0000000 --- a/rodi/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -.PHONY: build fixlinks -include .env - -build: - mkdocs build - ./fixlinks.sh - rm -rf .build - mkdir -p .build/blacksheep - mv site/* .build/blacksheep - echo "Ready to publish" - - -build-v1: - mkdocs build - VERSION="v1" ./fixlinks.sh - rm -rf .build - mkdir -p .build/blacksheep/v1 - mv site/* .build/blacksheep/v1 - echo "Ready to publish" - - -clean: - rm -rf site/ - rm -rf .build/ From ebe88e25044118f54eae9453dbd91d72e38eb798 Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sat, 12 Apr 2025 10:48:48 +0200 Subject: [PATCH 12/13] Correct about --- rodi/docs/about.md | 11 ++++++++--- rodi/mkdocs.yml | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/rodi/docs/about.md b/rodi/docs/about.md index d69dca1..e818ae5 100644 --- a/rodi/docs/about.md +++ b/rodi/docs/about.md @@ -1,9 +1,14 @@ # About Rodi -... +Rodi born from the desire of using a `non-intrusive` implementation of +Dependency Injection for Python (that does not require modifying the code of +types it resolved using decorators, like most others existing implementations +of DI for Python). Type annotations make explicit decorators superfluous as +the DI container can inspect the code to obtain all information it needs to +resolve types. ## The project's home The project is hosted in [GitHub](https://github.com/Neoteroi/rodi), -handled following DevOps good practices, features 100% code coverage, and is -published to [pypi.org](https://pypi.org/project/rodi/). +handled following DevOps good practices, and is published to +[pypi.org](https://pypi.org/project/rodi/). diff --git a/rodi/mkdocs.yml b/rodi/mkdocs.yml index 52abdb3..2d8bf35 100644 --- a/rodi/mkdocs.yml +++ b/rodi/mkdocs.yml @@ -15,6 +15,7 @@ nav: - Context managers: context-managers.md - Union types: union-types.md - Errors: errors.md + - About Rodi: about.md - Neoteroi docs home: "/" theme: From b109a418d207a930605e518371edc2f1d13b237b Mon Sep 17 00:00:00 2001 From: Roberto Prevato Date: Sat, 12 Apr 2025 10:55:31 +0200 Subject: [PATCH 13/13] Update about.md --- rodi/docs/about.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rodi/docs/about.md b/rodi/docs/about.md index e818ae5..bb19eb7 100644 --- a/rodi/docs/about.md +++ b/rodi/docs/about.md @@ -1,12 +1,15 @@ # About Rodi Rodi born from the desire of using a `non-intrusive` implementation of -Dependency Injection for Python (that does not require modifying the code of +Dependency Injection for Python, that does not require modifying the code of types it resolved using decorators, like most others existing implementations -of DI for Python). Type annotations make explicit decorators superfluous as +of DI for Python. Type annotations make explicit decorators superfluous as the DI container can inspect the code to obtain all information it needs to resolve types. +Rodi is the built-in DI framework in the [BlackSheep](/blacksheep/) web +framework, although it can be replaced with alternative solutions if desired. + ## The project's home The project is hosted in [GitHub](https://github.com/Neoteroi/rodi),