Skip to content

Commit

Permalink
Prepare 0.3.0 version (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
yakimka authored Apr 24, 2024
1 parent 4c87865 commit 375640d
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 11 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ We follow [Semantic Versions](https://semver.org/).

- Removed `use_cache` parameter
- Added tests

## Version 0.3.0

- Internal refactoring
- Updated README.md
257 changes: 247 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,276 @@
# picodi
# Picodi - Python DI (Dependency Injection) Library

[![Build Status](https://github.com/yakimka/picodi/actions/workflows/workflow-ci.yml/badge.svg?branch=main&event=push)](https://github.com/yakimka/picodi/actions/workflows/workflow-ci.yml)
[![Codecov](https://codecov.io/gh/yakimka/picodi/branch/main/graph/badge.svg)](https://codecov.io/gh/yakimka/picodi)
[![PyPI - Version](https://img.shields.io/pypi/v/picodi.svg)](https://pypi.org/project/picodi/)
![PyPI - Downloads](https://img.shields.io/pypi/dm/picodi)

Simple Dependency Injection for Python
Simple Dependency Injection for Python.
This library supports both synchronous and asynchronous contexts
and offers features like resource lifecycle management.

Experimental dependency injection library for Python. Use it at your own risk.
## Status

Picodi is currently in the experimental stage.
Public APIs may change without notice until the library reaches a 1.x.x version.
Use it at your own risk.

## Installation

```bash
pip install picodi
```

## Example
## Features

- 🌟 Simple and lightweight
- 📦 Zero dependencies
- ⏱️ Supports both sync and async contexts
- 🔄 Resource lifecycle management
- 🔍 Type hints support
- 🐍 Python 3.10+

## Basic Usage

Picodi uses decorators, functions and generators to provide and inject dependencies:

### Declaring dependencies

Dependencies can be simple functions or generators that act as context managers.

```python
import asyncio
import random


# this is a simple function that returns a random port number
# and this function can be used as a dependency
def get_random_db_port() -> int:
return random.randint(1024, 49151)


# or async version
async def get_random_db_port_async() -> int:
await asyncio.sleep(1)
return random.randint(1024, 49151)
```

### Injecting dependencies

Declare dependencies in function arguments using the `Provide` function.
Use the `inject` decorator if you want to resolve `Provide` dependencies on function call:

```python
import random

from picodi import inject, Provide


def get_redis() -> str:
yield "redis"
print("closing redis")
def get_random_db_port() -> int:
return random.randint(1024, 49151)


@inject
def get_connection_settings(port: int = Provide(get_random_db_port)):
return {"port": port}

# get_connection_settings() will return
# a dictionary with a random port number on every call
```

### Declaring dependencies that acts like a context manager

You can also use a generator to declare dependencies that need to be cleaned up after use:

```python
from typing import Generator

from picodi import Provide, inject


# also can be async, just use `async def`
def get_db() -> Generator[str, None, None]:
yield "db connection"
print("closing db connection")


@inject
def process_data(db: str = Provide(get_db)) -> None:
print("processing data in db:", db)

process_data()
# -> processing data in db: db connection
# -> closing db connection
```

### Declaring resource dependencies

Use the `resource` decorator to declare a resource,
which ensures that the provided function is treated as a singleton
and that its lifecycle is managed across the application:

```python
import asyncio
import random
from typing import AsyncGenerator

from picodi import Provide, inject, resource, shutdown_resources


# useful for managing resources like connections
@resource
async def get_db_port() -> AsyncGenerator[int, None]:
yield random.randint(1024, 49151)
print("closing db port")


@inject
def get_storage_service(redis: str = Provide(get_redis)) -> str:
return f"storage_service({redis})"
async def check_port(port: int = Provide(get_db_port)) -> None:
print("checking port:", port)


async def main() -> None:
await check_port()
await check_port()
print("shutting down resources")
# resources need to be closed manually
await shutdown_resources()

assert get_storage_service() == "storage_service(redis)"

asyncio.run(main())
# -> checking port: 24090
# -> checking port: 24090
# -> shutting down resources
# -> closing db port
```

### Resolving async dependencies in sync functions

If you try to resolve async dependencies in sync functions, you may get not what you expect.

```python
import asyncio

from picodi import Provide, inject


async def get_db_port() -> int:
return 8080


@inject
def print_port(port: int = Provide(get_db_port)) -> None:
print("port is:", port)


async def main() -> None:
print_port()


asyncio.run(main())
# -> port is: <coroutine object get_db_port at 0x1037741a0>
```

But if your dependency is a resource,
you can use `init_resources` on startup to resolve dependencies and then use cached values,
even in sync functions:

```python
import asyncio
from typing import AsyncGenerator

from picodi import Provide, init_resources, inject, resource

@resource
async def get_db_port() -> AsyncGenerator[int, None]:
yield 8080


@inject
def print_port(port: int = Provide(get_db_port)) -> None:
print("port is:", port)


async def main() -> None:
await init_resources()
print_port()


asyncio.run(main())
# -> port is: 8080
```

## Using picodi with web frameworks

Picodi can be used with web frameworks like FastAPI or Django.

```python
import random

from fastapi import FastAPI
from picodi import Provide, inject

app = FastAPI()


def get_random_int():
yield random.randint(1, 100)


@inject
async def get_redis_connection(port: int = Provide(get_random_int)) -> str:
return "http://redis:{}".format(port)


@app.get("/")
@inject
async def read_root(redis: str = Provide(get_redis_connection)):
return {"redis": redis}


# uvicorn fastapi_di:app --reload
```

## API Reference

### `Provide(dependency)`

Marks a callable as a provider of a dependency.
It manages the lifecycle of the dependency,
including initialization and teardown if the dependency is a generator.

- **Parameters**:
- `dependency`: A callable that returns the dependency or a generator for context management.

### `inject(fn)`

Decorator to automatically inject dependencies declared by `Provide` into a function.

- **Parameters**:
- `fn`: The function into which dependencies will be injected.

### `resource(fn)`

Decorator to declare a resource,
which ensures that the provided function is treated as a singleton
and that its lifecycle is managed across the application.

- **Parameters**:
- `fn`: A generator function that yields a resource.

### `init_resources()`

Initializes all declared resources. Typically called at the startup of the application.

Can be called as `init_resources()` in sync context and `await init_resources()` in async context.

### `shutdown_resources()`

Cleans up all resources.
It should be called when the application is shutting down to ensure proper resource cleanup.

Can be called as `shutdown_resources()` in sync context and `await shutdown_resources()` in async context.

## License

[MIT](https://github.com/yakimka/picodi/blob/main/LICENSE)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "picodi"
description = "Simple Dependency Injection for Python"
version = "0.2.0"
version = "0.3.0"
license = "MIT"
authors = [
"yakimka"
Expand Down
File renamed without changes.
File renamed without changes.

0 comments on commit 375640d

Please sign in to comment.