Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Александр Шибаев. Dependency injection: как исп...

Александр Шибаев. Dependency injection: как использовать и почему это упрощает разработку

В нашем коде существует большое количество зависимостей. Большинство разработчиков не запариваются, как ими управлять, и иногда это приводит к dependency hell — распространенному анти-паттерну разработки. Чтобы не оказаться в таком аду, полезно знать, какие существуют подходы в разработке кода.
Популярные библиотеки на python используют такие подходы, как threadlocal переменные или singleton объекты. Команда Тинькофф выбрала путь, который популярен в других языках программирования, и наработала на нашем большом проекте опыт, которым не стыдно поделиться. Речь о dependency injection. В своем докладе я разберу:
- как использовать dependency injection контейнер в python c библиотекой punq;
- как прикрутить ее к pydantic и fastapi;
- каким образом это упрощает разработку и помогает в написании кода с аннотациями типов.

More Decks by Python Community Chelyabinsk

Other Decks in Programming

Transcript

  1. bp = Blueprint("blog", __name__) @bp.route("/") def index(): """Show all the

    posts, most recent first.""" db = get_db() posts = db.execute( "SELECT p.id, title, body, created, author_id, usernam " FROM post p JOIN user u ON p.author_id = u.id" " ORDER BY created DESC" ).fetchall() return render_template("blog/index.html", posts=posts)
  2. def get_db(): if "db" not in g: g.db = sqlite3.connect(

    current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES ) g.db.row_factory = sqlite3.Row return g.db
  3. def close_db(e=None): db = g.pop("db", None) if db is not

    None: db.close() def init_app(app): app.teardown_appcontext(close_db)
  4. def create_app(test_config=None): ... if test_config is None: # load the

    instance config, if it exists, when not tes app.config.from_pyfile("config.py", silent=True) else: # load the test config if passed in app.config.update(test_config) ... return app
  5. @pytest.fixture def app(): db_fd, db_path = tempfile.mkstemp() app = create_app({"TESTING":

    True, "DATABASE": db_path}) with app.app_context(): init_db() yield app os.close(db_fd) @pytest.fixture def client(app): with app.test_client() as client: yield client
  6. def test_index(client, auth): response = client.get("/") assert b"Log In" in

    response.data assert b"Register" in response.data auth.login() response = client.get("/") assert b"test title" in response.data assert b"by test on 2018-01-01" in response.data assert b"test\nbody" in response.data assert b'href="/1/update"' in response.data
  7. @app.route('/login', methods=['GET', 'POST']) def login(): if flask.request.method == 'GET': return

    ... email = flask.request.form['email'] password = flask.request.form['password'] if chech_password(email, password): user = User() user.id = email flask_login.login_user(user) return flask.redirect(flask.url_for('protected')) return 'Bad login'
  8. def login_user(user, ...): ... login_manager = current_app.login_manager user_id = getattr(user,

    login_manager.id_attribute)() session['_user_id'] = user_id session['_fresh'] = fresh session['_id'] = login_manager._session_identifier_generat ... current_app.login_manager._update_request_context_with_use ... return True
  9. app = FastAPI() @app.put("/items/{item_id}") async def update_item(item_id: int, item: Item

    = Body(...)): results = {"item_id": item_id, "item": item} return results
  10. @app.put("/items/{item_id}") async def update_item( item_id: int, item: Item = Body(...),

    item_manager: ItemManager = Depends(get_item_manager) ) -> ItemUpdateModel: result = item_manager.update_item(item_id, item) return result
  11. def update_item( item_id: int, item_manager: ItemManager, request: Any = request,

    ) -> ItemUpdateModel: item = ItemModel.parse_obj(request.json) result = item_manager.update_item(item_id, item) return result
  12. class Factory: @property def item_manager() -> ItemManager: return ItemManager() def

    create_update_item_handler(self): return wraps(update_item)( partial( update_item, item_manager=self.item_manager ) )
  13. class Factory @property def session_factory() -> SessionFactory: return SessionFactory() @property

    def item_manager() -> ItemManager: return ItemManager(self.session_factory) ...
  14. @inject def main(service: Service = Provide[Container.service]): ... if __name__ ==

    '__main__': container = Container() container.config.api_key.from_env('API_KEY') container.config.timeout.from_env('TIMEOUT') container.wire(modules=[sys.modules[__name__]]) main()
  15. Минусы Минусы singleton по умолчанию может модифицироваться в рантайме много

    ручной работы позволяет внедрять аргументы функциям
  16. >>> class EmailSender: ... def send(self, msg): ... pass ...

    >>> class SmtpEmailSender(EmailSender): ... def send(self, msg): ... print("Sending message via smtp") ... >>> container.register(EmailSender, SmtpEmailSender) >>> instance = container.resolve(EmailSender) >>> instance.send("beep") Sending message via smtp
  17. class A: pass class B(A): pass class C: def __init__(self,

    a: A): pass container.register(A, B) container.register(C, C) container.resolve(C)
  18. Хотелка номер 3 Хотелка номер 3 Хотим, чтобы можно было

    получить финальный контейнер, который нельзя менять
  19. class A(Generic[T]): def __init__(self, value: T): self.value = value class

    B(Generic[T]): def __init__(self, value: T): self.value = value class C(Generic[T]): def __init__(self, value: A[B[T]]): self.value = value container = Container() container.singleton(B[int], B[int], value=1) container.singleton(A[B[int]], A[B[int]]) container.singleton(C[int], C[int])
  20. from typing import get_args, get_origin T = TypeVar('T') class A(Generic[T]):

    pass assert get_args(A[int]) == (int,) assert get_origin(A[int]) == A assert A.__parameters__ == (T,)
  21. Что мы получаем за все наши Что мы получаем за

    все наши страдания? страдания? Кроме того, что нам удобно тестировать?
  22. class Factory: def __init__(self, container: FinalizedContainer): self._container = container def

    create_some_handler(self) -> Callable[[Data], None]: return partial( some_handler, first_client=self._container.resolve(IClient), manager=self._container.resolve(IManager), ... )
  23. Traceback (most recent call last): ... pydantic.error_wrappers.ValidationError: 1 validation error

    for ClientSettings url field required (type=value_error.missing)
  24. def collect_errors(container: Container): registered = container.registrations.registrations.keys() errors = [] validation_errors

    = {} for key in registered: try: container.resolve(key) except ValidationError as exc: validation_errors[str(exc.model)] = exc except Exception as exc: errors.append((key, exc)) return validation_errors