mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2024-11-29 13:04:02 +03:00
Merge branch 'release/3.20.0' into master
This commit is contained in:
commit
21cdd0aa3c
458
README.rst
458
README.rst
|
@ -1,28 +1,48 @@
|
|||
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/logo.svg
|
||||
:target: https://github.com/ets-labs/python-dependency-injector
|
||||
|
||||
|
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/dependency_injector.svg
|
||||
:target: https://pypi.org/project/dependency-injector/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/l/dependency_injector.svg
|
||||
:target: https://pypi.org/project/dependency-injector/
|
||||
:alt: License
|
||||
.. image:: https://pepy.tech/badge/dependency-injector
|
||||
:target: https://pepy.tech/project/dependency-injector
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/dependency_injector.svg
|
||||
:target: https://pypi.org/project/dependency-injector/
|
||||
:alt: Supported Python versions
|
||||
|
||||
.. image:: https://img.shields.io/pypi/implementation/dependency_injector.svg
|
||||
:target: https://pypi.org/project/dependency-injector/
|
||||
:alt: Supported Python implementations
|
||||
|
||||
.. image:: https://pepy.tech/badge/dependency-injector
|
||||
:target: https://pepy.tech/project/dependency-injector
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: https://pepy.tech/badge/dependency-injector/month
|
||||
:target: https://pepy.tech/project/dependency-injector
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: https://pepy.tech/badge/dependency-injector/week
|
||||
:target: https://pepy.tech/project/dependency-injector
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: https://img.shields.io/pypi/wheel/dependency-injector.svg
|
||||
:target: https://pypi.org/project/dependency-injector/
|
||||
:alt: Wheel
|
||||
|
||||
.. image:: https://travis-ci.org/ets-labs/python-dependency-injector.svg?branch=master
|
||||
:target: https://travis-ci.org/ets-labs/python-dependency-injector
|
||||
:alt: Build Status
|
||||
|
||||
.. image:: http://readthedocs.org/projects/python-dependency-injector/badge/?version=latest
|
||||
:target: http://python-dependency-injector.ets-labs.org/
|
||||
:alt: Docs Status
|
||||
|
||||
.. image:: https://coveralls.io/repos/github/ets-labs/python-dependency-injector/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/ets-labs/python-dependency-injector?branch=master
|
||||
:alt: Coverage Status
|
||||
|
@ -30,370 +50,100 @@
|
|||
What is ``Dependency Injector``?
|
||||
================================
|
||||
|
||||
``Dependency Injector`` is a dependency injection microframework for Python.
|
||||
It was designed to be a unified and developer-friendly tool that helps
|
||||
implement a dependency injection design pattern in a formal, pretty, and
|
||||
Pythonic way.
|
||||
``Dependency Injector`` is a dependency injection framework for Python.
|
||||
|
||||
The key features of the *Dependency Injector* framework are:
|
||||
Why do I need it?
|
||||
=================
|
||||
|
||||
+ Easy, smart, and pythonic style.
|
||||
+ Obvious and clear structure.
|
||||
+ Extensibility and flexibility.
|
||||
+ High performance.
|
||||
+ Memory efficiency.
|
||||
+ Thread safety.
|
||||
+ Documented.
|
||||
+ Semantically versioned.
|
||||
``Dependency Injector`` helps you improve application structure.
|
||||
|
||||
*Dependency Injector* containers and providers are implemented as C extension
|
||||
types using Cython.
|
||||
With the ``Dependency Injector`` you keep **application structure in one place**.
|
||||
This place is called **the container**. You use the container to manage all the components of the application. All the component dependencies are defined explicitly. This provides the control on the application structure. It is **easy to understand and change** it.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-map.svg
|
||||
:target: https://github.com/ets-labs/python-dependency-injector
|
||||
|
||||
*The container is like a map of your application. You always know what depends on what.*
|
||||
|
||||
``Flask`` + ``Dependency Injector`` example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import flask
|
||||
from github import Github
|
||||
|
||||
from . import services, views
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
class Application(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
The *Dependency Injector* library is available on `PyPi`_::
|
||||
config = providers.Configuration()
|
||||
|
||||
github_client = providers.Factory(
|
||||
Github,
|
||||
login_or_token=config.github.auth_token,
|
||||
timeout=config.github.request_timeout,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
github_client=github_client,
|
||||
)
|
||||
|
||||
index_view = providers.Callable(
|
||||
views.index,
|
||||
search_service=search_service,
|
||||
default_search_term=config.search.default_term,
|
||||
default_search_limit=config.search.default_limit,
|
||||
)
|
||||
|
||||
app = providers.Factory(
|
||||
flask.create_app,
|
||||
name=__name__,
|
||||
routes=[
|
||||
flask.Route('/', view_provider=index_view),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
See complete example here - `Flask + Dependency Injector Example <https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/ghnav-flask>`_
|
||||
|
||||
How to install?
|
||||
---------------
|
||||
|
||||
- The package is available on the `PyPi`_::
|
||||
|
||||
pip install dependency-injector
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
The *Dependency Injector* documentation is hosted on ReadTheDocs:
|
||||
|
||||
- `User's guide`_
|
||||
- `API docs`_
|
||||
|
||||
Dependency injection
|
||||
--------------------
|
||||
|
||||
`Dependency injection`_ is a software design pattern that implements
|
||||
`Inversion of control`_ to resolve dependencies. Formally, if object **A**
|
||||
depends on object **B**, object **A** must not create or import object **B**
|
||||
directly. Instead of this object **A** must provide a way to *inject*
|
||||
object **B**. The responsibilities of objects creation and dependency
|
||||
injection are delegated to external code - the *dependency injector*.
|
||||
|
||||
Popular terminology of the dependency injection pattern:
|
||||
|
||||
+ Object **A**, which depends on object **B**, is often called -
|
||||
the *client*.
|
||||
+ Object **B**, which is depended on, is often called - the *service*.
|
||||
+ External code that is responsible for creation of objects and injection
|
||||
of dependencies is often called - the *dependency injector*.
|
||||
|
||||
There are several ways to inject a *service* into a *client*:
|
||||
|
||||
+ by passing it as an ``__init__`` argument (constructor / initializer
|
||||
injection)
|
||||
+ by setting it as an attribute's value (attribute injection)
|
||||
+ by passing it as a method's argument (method injection)
|
||||
|
||||
The dependency injection pattern has few strict rules that should be followed:
|
||||
|
||||
+ The *client* delegates to the *dependency injector* the responsibility
|
||||
of injecting its dependencies - the *service(s)*.
|
||||
+ The *client* doesn't know how to create the *service*, it knows only
|
||||
the interface of the *service*. The *service* doesn't know that it is used by
|
||||
the *client*.
|
||||
+ The *dependency injector* knows how to create the *client* and
|
||||
the *service*. It also knows that the *client* depends on the *service*,
|
||||
and knows how to inject the *service* into the *client*.
|
||||
+ The *client* and the *service* know nothing about the *dependency injector*.
|
||||
|
||||
The dependency injection pattern provides the following advantages:
|
||||
|
||||
+ Control of application structure.
|
||||
+ Decreased coupling of application components.
|
||||
+ Increased code reusability.
|
||||
+ Increased testability.
|
||||
+ Increased maintainability.
|
||||
+ Reconfiguration of a system without rebuilding.
|
||||
|
||||
Example of dependency injection
|
||||
-------------------------------
|
||||
|
||||
Let's go through next example:
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/engines_cars/diagram.png
|
||||
:width: 100%
|
||||
:align: center
|
||||
|
||||
Listing of ``example.engines`` module:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""Dependency injection example, engines module."""
|
||||
|
||||
|
||||
class Engine:
|
||||
"""Example engine base class.
|
||||
|
||||
Engine is a heart of every car. Engine is a very common term and could be
|
||||
implemented in very different ways.
|
||||
"""
|
||||
|
||||
|
||||
class GasolineEngine(Engine):
|
||||
"""Gasoline engine."""
|
||||
|
||||
|
||||
class DieselEngine(Engine):
|
||||
"""Diesel engine."""
|
||||
|
||||
|
||||
class ElectricEngine(Engine):
|
||||
"""Electric engine."""
|
||||
|
||||
Listing of ``example.cars`` module:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""Dependency injection example, cars module."""
|
||||
|
||||
|
||||
class Car:
|
||||
"""Example car."""
|
||||
|
||||
def __init__(self, engine):
|
||||
"""Initializer."""
|
||||
self._engine = engine # Engine is injected
|
||||
|
||||
The next example demonstrates the creation of several cars with different engines:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""Dependency injection example, Cars & Engines."""
|
||||
|
||||
import example.cars
|
||||
import example.engines
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
gasoline_car = example.cars.Car(example.engines.GasolineEngine())
|
||||
diesel_car = example.cars.Car(example.engines.DieselEngine())
|
||||
electric_car = example.cars.Car(example.engines.ElectricEngine())
|
||||
|
||||
While the previous example demonstrates the advantages of dependency injection,
|
||||
there is a disadvantage demonstrated as well - the creation of a car requires
|
||||
additional code to specify its dependencies. However, this disadvantage
|
||||
could be avoided by using a dependency injection framework for the creation of
|
||||
an inversion of control container (IoC container).
|
||||
|
||||
Here's an example of the creation of several inversion of control containers
|
||||
(IoC containers) using *Dependency Injector*:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""Dependency injection example, Cars & Engines IoC containers."""
|
||||
|
||||
import example.cars
|
||||
import example.engines
|
||||
|
||||
import dependency_injector.containers as containers
|
||||
import dependency_injector.providers as providers
|
||||
|
||||
|
||||
class Engines(containers.DeclarativeContainer):
|
||||
"""IoC container of engine providers."""
|
||||
|
||||
gasoline = providers.Factory(example.engines.GasolineEngine)
|
||||
|
||||
diesel = providers.Factory(example.engines.DieselEngine)
|
||||
|
||||
electric = providers.Factory(example.engines.ElectricEngine)
|
||||
|
||||
|
||||
class Cars(containers.DeclarativeContainer):
|
||||
"""IoC container of car providers."""
|
||||
|
||||
gasoline = providers.Factory(example.cars.Car,
|
||||
engine=Engines.gasoline)
|
||||
|
||||
diesel = providers.Factory(example.cars.Car,
|
||||
engine=Engines.diesel)
|
||||
|
||||
electric = providers.Factory(example.cars.Car,
|
||||
engine=Engines.electric)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
gasoline_car = Cars.gasoline()
|
||||
diesel_car = Cars.diesel()
|
||||
electric_car = Cars.electric()
|
||||
|
||||
Dependency Injector structure
|
||||
-----------------------------
|
||||
|
||||
*Dependency Injector* is a microframework and has a simple structure.
|
||||
|
||||
There are two main entities: providers and containers.
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/internals.png
|
||||
:width: 100%
|
||||
:align: center
|
||||
|
||||
Providers
|
||||
~~~~~~~~~
|
||||
|
||||
Providers describe strategies of accessing objects. They define how particular
|
||||
objects are provided.
|
||||
|
||||
- **Provider** - base provider class.
|
||||
- **Callable** - provider that calls a wrapped callable on every call. Supports
|
||||
positional and keyword argument injections.
|
||||
- **Factory** - provider that creates new instance of specified class on every
|
||||
call. Supports positional and keyword argument injections, as well as
|
||||
attribute injections.
|
||||
- **Singleton** - provider that creates new instance of specified class on its
|
||||
first call and returns the same instance on every next call. Supports
|
||||
position and keyword argument injections, as well as attribute injections.
|
||||
- **Object** - provider that returns provided instance "as is".
|
||||
- **ExternalDependency** - provider that can be useful for development of
|
||||
self-sufficient libraries, modules, and applications that require external
|
||||
dependencies.
|
||||
- **Configuration** - provider that helps with implementing late static binding
|
||||
of configuration options - use first, define later.
|
||||
|
||||
Containers
|
||||
~~~~~~~~~~
|
||||
|
||||
Containers are collections of providers. The main purpose of containers is to
|
||||
group providers.
|
||||
|
||||
- **DeclarativeContainer** - is an inversion of control container that can be
|
||||
defined in a declarative manner. It covers most of the cases where a list of
|
||||
providers that is be included in a container is deterministic
|
||||
(that means the container will not change its structure in runtime).
|
||||
- **DynamicContainer** - is an inversion of control container with a dynamic
|
||||
structure. It covers most of the cases where a list of providers that
|
||||
would be included in container is non-deterministic and depends on the
|
||||
application's flow or its configuration (container's structure could be
|
||||
determined just after the application starts and might perform some initial
|
||||
work, like parsing a list of container providers from a configuration).
|
||||
|
||||
Dependency Injector in action
|
||||
-----------------------------
|
||||
|
||||
The brief example below is a simplified version of inversion of control
|
||||
containers from a real-life application. The example demonstrates the usage
|
||||
of *Dependency Injector* inversion of control container and providers for
|
||||
specifying application components and their dependencies on each other in one
|
||||
module. Besides other previously mentioned advantages, it shows a great
|
||||
opportunity to control and manage application's structure in one place.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""Example of dependency injection in Python."""
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
|
||||
import boto3
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from example import services, main
|
||||
|
||||
|
||||
class IocContainer(containers.DeclarativeContainer):
|
||||
"""Application IoC container."""
|
||||
|
||||
config = providers.Configuration('config')
|
||||
logger = providers.Singleton(logging.Logger, name='example')
|
||||
|
||||
# Gateways
|
||||
|
||||
database_client = providers.Singleton(sqlite3.connect, config.database.dsn)
|
||||
|
||||
s3_client = providers.Singleton(
|
||||
boto3.client, 's3',
|
||||
aws_access_key_id=config.aws.access_key_id,
|
||||
aws_secret_access_key=config.aws.secret_access_key,
|
||||
)
|
||||
|
||||
# Services
|
||||
|
||||
users_service = providers.Factory(
|
||||
services.UsersService,
|
||||
db=database_client,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
auth_service = providers.Factory(
|
||||
services.AuthService,
|
||||
token_ttl=config.auth.token_ttl,
|
||||
db=database_client,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
photos_service = providers.Factory(
|
||||
services.PhotosService,
|
||||
db=database_client,
|
||||
s3=s3_client,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
# Misc
|
||||
|
||||
main = providers.Callable(
|
||||
main.main,
|
||||
users_service=users_service,
|
||||
auth_service=auth_service,
|
||||
photos_service=photos_service,
|
||||
)
|
||||
|
||||
The next example demonstrates a run of the example application defined above:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""Run example of dependency injection in Python."""
|
||||
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from container import IocContainer
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Configure container:
|
||||
container = IocContainer(
|
||||
config={
|
||||
'database': {
|
||||
'dsn': ':memory:',
|
||||
},
|
||||
'aws': {
|
||||
'access_key_id': 'KEY',
|
||||
'secret_access_key': 'SECRET',
|
||||
},
|
||||
'auth': {
|
||||
'token_ttl': 3600,
|
||||
},
|
||||
}
|
||||
)
|
||||
container.logger().addHandler(logging.StreamHandler(sys.stdout))
|
||||
|
||||
# Run application:
|
||||
container.main(*sys.argv[1:])
|
||||
|
||||
You can find more *Dependency Injector* examples in the ``/examples`` directory
|
||||
on our GitHub:
|
||||
|
||||
https://github.com/ets-labs/python-dependency-injector
|
||||
|
||||
Feedback & Support
|
||||
Where is the docs?
|
||||
------------------
|
||||
|
||||
Feel free to post questions, bugs, feature requests, proposals, etc. on
|
||||
the *Dependency Injector* GitHub issues page:
|
||||
- The documentation is available on the `Read The Docs <http://python-dependency-injector.ets-labs.org/>`_
|
||||
|
||||
https://github.com/ets-labs/python-dependency-injector/issues
|
||||
Have a question?
|
||||
----------------
|
||||
|
||||
Your feedback is quite important!
|
||||
- Open a `Github Issue <https://github.com/ets-labs/python-dependency-injector/issues>`_
|
||||
|
||||
Found a bug?
|
||||
------------
|
||||
|
||||
- Open a `Github Issue <https://github.com/ets-labs/python-dependency-injector/issues>`_
|
||||
|
||||
Want to help?
|
||||
-------------
|
||||
|
||||
- ⭐️ Star the ``Dependency Injector`` on the `Github <https://github.com/ets-labs/python-dependency-injector/>`_
|
||||
- 🆕 Start a new project with the ``Dependency Injector``
|
||||
- 💬 Tell your friend about the ``Dependency Injector``
|
||||
|
||||
|
||||
.. _Dependency injection: http://en.wikipedia.org/wiki/Dependency_injection
|
||||
.. _Inversion of control: https://en.wikipedia.org/wiki/Inversion_of_control
|
||||
Want to contribute?
|
||||
-------------------
|
||||
|
||||
- 🔀 Fork the project
|
||||
- ⬅️ Open a pull request to the ``develop`` branch
|
||||
|
||||
.. _PyPi: https://pypi.org/project/dependency-injector/
|
||||
.. _User's guide: http://python-dependency-injector.ets-labs.org/
|
||||
.. _API docs: http://python-dependency-injector.ets-labs.org/api/
|
||||
|
|
9
docs/api/flaskext.rst
Normal file
9
docs/api/flaskext.rst
Normal file
|
@ -0,0 +1,9 @@
|
|||
``dependency_injector.ext.flask``
|
||||
---------------------------------
|
||||
|
||||
.. automodule:: dependency_injector.ext.flask
|
||||
:members:
|
||||
:special-members:
|
||||
|
||||
|
||||
.. disqus::
|
|
@ -8,3 +8,4 @@ API Documentation
|
|||
providers
|
||||
containers
|
||||
errors
|
||||
flaskext
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
======================================================================
|
||||
Dependency Injector --- Dependency injection microframework for Python
|
||||
======================================================================
|
||||
=================================================================
|
||||
Dependency Injector --- Dependency injection framework for Python
|
||||
=================================================================
|
||||
|
||||
.. meta::
|
||||
:google-site-verification: 6it89zX0_wccKEhAqbAiYQooS95f0BA8YfesHk6bsNA
|
||||
|
@ -22,24 +22,43 @@ Dependency Injector --- Dependency injection microframework for Python
|
|||
.. image:: https://img.shields.io/pypi/v/dependency_injector.svg
|
||||
:target: https://pypi.org/project/dependency-injector/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/l/dependency_injector.svg
|
||||
:target: https://pypi.org/project/dependency-injector/
|
||||
:alt: License
|
||||
.. image:: https://pepy.tech/badge/dependency-injector
|
||||
:target: https://pepy.tech/project/dependency-injector
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/dependency_injector.svg
|
||||
:target: https://pypi.org/project/dependency-injector/
|
||||
:alt: Supported Python versions
|
||||
|
||||
.. image:: https://img.shields.io/pypi/implementation/dependency_injector.svg
|
||||
:target: https://pypi.org/project/dependency-injector/
|
||||
:alt: Supported Python implementations
|
||||
|
||||
.. image:: https://pepy.tech/badge/dependency-injector
|
||||
:target: https://pepy.tech/project/dependency-injector
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: https://pepy.tech/badge/dependency-injector/month
|
||||
:target: https://pepy.tech/project/dependency-injector
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: https://pepy.tech/badge/dependency-injector/week
|
||||
:target: https://pepy.tech/project/dependency-injector
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: https://img.shields.io/pypi/wheel/dependency-injector.svg
|
||||
:target: https://pypi.org/project/dependency-injector/
|
||||
:alt: Wheel
|
||||
|
||||
.. image:: https://travis-ci.org/ets-labs/python-dependency-injector.svg?branch=master
|
||||
:target: https://travis-ci.org/ets-labs/python-dependency-injector
|
||||
:alt: Build Status
|
||||
|
||||
.. image:: http://readthedocs.org/projects/python-dependency-injector/badge/?version=latest
|
||||
:target: http://python-dependency-injector.ets-labs.org/
|
||||
:alt: Docs Status
|
||||
|
||||
.. image:: https://coveralls.io/repos/github/ets-labs/python-dependency-injector/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/ets-labs/python-dependency-injector?branch=master
|
||||
:alt: Coverage Status
|
||||
|
|
|
@ -7,6 +7,13 @@ that were made in every particular version.
|
|||
From version 0.7.6 *Dependency Injector* framework strictly
|
||||
follows `Semantic versioning`_
|
||||
|
||||
3.20.0
|
||||
------
|
||||
- Add ``Flask`` integration module ``dependency_injector.flask.ext``.
|
||||
- Add ``Flask`` + ``Dependency Injector`` example ``ghnav-flask``.
|
||||
- Add ``Factory.provides`` attribute. It is an alias to the ``Factory.cls``.
|
||||
- New README.
|
||||
|
||||
3.19.2
|
||||
------
|
||||
- Add logo.
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
[pydocstyle]
|
||||
ignore = D101,D203,D213
|
||||
ignore = D101,D103,D107,D203,D213
|
||||
|
|
90
examples/miniapps/ghnav-flask/README.rst
Normal file
90
examples/miniapps/ghnav-flask/README.rst
Normal file
|
@ -0,0 +1,90 @@
|
|||
Flask Dependency Injection Example
|
||||
==================================
|
||||
|
||||
Application ``githubnavigator`` is a `Flask <https://flask.palletsprojects.com/>`_ +
|
||||
`Dependency Injector <http://python-dependency-injector.ets-labs.org/>`_ application.
|
||||
|
||||
Run
|
||||
---
|
||||
|
||||
Install requirements:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
To run the application do:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export FLASK_APP=githubnavigator.entrypoint
|
||||
export FLASK_ENV=development
|
||||
flask run
|
||||
|
||||
The output should be something like:
|
||||
|
||||
.. code-block::
|
||||
|
||||
* Serving Flask app "githubnavigator.entrypoint" (lazy loading)
|
||||
* Environment: development
|
||||
* Debug mode: on
|
||||
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
|
||||
* Restarting with fsevents reloader
|
||||
* Debugger is active!
|
||||
* Debugger PIN: 473-587-859
|
||||
|
||||
After that visit http://127.0.0.1:5000/ in your browser.
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
Github has a rate limit. When thre rate limit is exceed you will see an exception
|
||||
``github.GithubException.RateLimitExceededException``. For unauthenticated requests, the rate
|
||||
limit allows for up to 60 requests per hour. To extend the limit to 5000 requests per hour you
|
||||
need to set personal access token.
|
||||
|
||||
It's easy:
|
||||
|
||||
- Follow this `guide <https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token>`_ to create a token.
|
||||
- Set a token to the environment variable:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export GITHUB_TOKEN=<your token>
|
||||
|
||||
- Restart the app with ``flask run``
|
||||
|
||||
`Read more on Github rate limit <https://developer.github.com/v3/#rate-limiting>`_
|
||||
|
||||
Test
|
||||
----
|
||||
|
||||
This application comes with unit tests.
|
||||
|
||||
To run the tests do:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
py.test githubnavigator/tests.py --cov=githubnavigator
|
||||
|
||||
The output should be something like:
|
||||
|
||||
.. code-block::
|
||||
|
||||
platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1
|
||||
plugins: flask-1.0.0, cov-2.10.0, asyncio-0.14.0
|
||||
collected 2 items
|
||||
|
||||
githubnavigator/tests.py .. [100%]
|
||||
|
||||
---------- coverage: platform darwin, python 3.8.3-final-0 -----------
|
||||
Name Stmts Miss Cover
|
||||
----------------------------------------------------
|
||||
githubnavigator/__init__.py 0 0 100%
|
||||
githubnavigator/application.py 10 0 100%
|
||||
githubnavigator/entrypoint.py 5 5 0%
|
||||
githubnavigator/services.py 13 0 100%
|
||||
githubnavigator/tests.py 38 0 100%
|
||||
githubnavigator/views.py 7 0 100%
|
||||
----------------------------------------------------
|
||||
TOTAL 73 5 93%
|
5
examples/miniapps/ghnav-flask/config.yml
Normal file
5
examples/miniapps/ghnav-flask/config.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
github:
|
||||
request_timeout: 10
|
||||
search:
|
||||
default_term: "Dependency Injector"
|
||||
default_limit: 5
|
|
@ -0,0 +1 @@
|
|||
"""Top-level package."""
|
39
examples/miniapps/ghnav-flask/githubnavigator/application.py
Normal file
39
examples/miniapps/ghnav-flask/githubnavigator/application.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
"""Application module."""
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import flask
|
||||
from github import Github
|
||||
|
||||
from . import services, views
|
||||
|
||||
|
||||
class Application(containers.DeclarativeContainer):
|
||||
"""Application container."""
|
||||
|
||||
config = providers.Configuration()
|
||||
|
||||
github_client = providers.Factory(
|
||||
Github,
|
||||
login_or_token=config.github.auth_token,
|
||||
timeout=config.github.request_timeout,
|
||||
)
|
||||
|
||||
search_service = providers.Factory(
|
||||
services.SearchService,
|
||||
github_client=github_client,
|
||||
)
|
||||
|
||||
index_view = providers.Callable(
|
||||
views.index,
|
||||
search_service=search_service,
|
||||
default_search_term=config.search.default_term,
|
||||
default_search_limit=config.search.default_limit,
|
||||
)
|
||||
|
||||
app = providers.Factory(
|
||||
flask.create_app,
|
||||
name=__name__,
|
||||
routes=[
|
||||
flask.Route('/', view_provider=index_view),
|
||||
],
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
"""Entrypoint module."""
|
||||
|
||||
from .application import Application
|
||||
|
||||
|
||||
application = Application()
|
||||
application.config.from_yaml('config.yml')
|
||||
application.config.github.auth_token.from_env('GITHUB_TOKEN')
|
||||
app = application.app()
|
41
examples/miniapps/ghnav-flask/githubnavigator/services.py
Normal file
41
examples/miniapps/ghnav-flask/githubnavigator/services.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
"""Services module."""
|
||||
|
||||
from github import Github
|
||||
from github.Repository import Repository
|
||||
from github.Commit import Commit
|
||||
|
||||
|
||||
class SearchService:
|
||||
"""Search service performs search on Github."""
|
||||
|
||||
def __init__(self, github_client: Github):
|
||||
self._github_client = github_client
|
||||
|
||||
def search_repositories(self, term, limit):
|
||||
"""Search for repositories and return formatted data."""
|
||||
repositories = self._github_client.search_repositories(term, **{'in': 'name'})
|
||||
return [
|
||||
self._format_repo(repository)
|
||||
for repository in repositories[:limit]
|
||||
]
|
||||
|
||||
def _format_repo(self, repository: Repository):
|
||||
return {
|
||||
'url': repository.html_url,
|
||||
'name': repository.name,
|
||||
'owner': {
|
||||
'login': repository.owner.login,
|
||||
'url': repository.owner.html_url,
|
||||
'avatar_url': repository.owner.avatar_url,
|
||||
},
|
||||
'created_at': repository.created_at,
|
||||
'latest_commit': self._format_commit(repository.get_commits()[0]),
|
||||
}
|
||||
|
||||
def _format_commit(self, commit: Commit):
|
||||
return {
|
||||
'sha': commit.sha,
|
||||
'url': commit.html_url,
|
||||
'message': commit.commit.message,
|
||||
'author_name': commit.commit.author.name,
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Github Navigator</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Github Navigator</h1>
|
||||
<form method="get">
|
||||
<p>
|
||||
Search term: <input type="text" name="search_term" value="{{ search_term if search_term }}">
|
||||
Limit: <input type="text" name="limit" value="{{ limit }}">
|
||||
<input type="submit">
|
||||
</p>
|
||||
</form>
|
||||
<h2>Search results</h2>
|
||||
{% if repositories|length == 0 %}
|
||||
<small>No search results</small>
|
||||
{% endif %}
|
||||
{% for repository in repositories %} {{n}}
|
||||
<p>
|
||||
<small>Search result # {{ loop.index }} from {{ repositories|length }}</small>
|
||||
</p>
|
||||
<p>
|
||||
Repository: <a href="{{ repository.url }}">{{ repository.name }}</a>
|
||||
</p>
|
||||
<p>
|
||||
Repository owner:
|
||||
<a href="{{ repository.owner.url }}"><img src="{{ repository.owner.avatar_url }}" alt="avatar" height="24" width="24"/></a>
|
||||
<a href="{{ repository.owner.url }}">{{ repository.owner.login }}</a>
|
||||
</p>
|
||||
<p>
|
||||
Created at: {{ repository.created_at }}
|
||||
</p>
|
||||
<p>
|
||||
LastCommit: <a href="{{ repository.latest_commit.url }}">{{ repository.latest_commit.sha }}</a> {{ repository.latest_commit['message'] }} {{ repository.latest_commit.author_name }}
|
||||
</p>
|
||||
<hr/>
|
||||
{% endfor %}
|
||||
</body>
|
||||
</html>
|
90
examples/miniapps/ghnav-flask/githubnavigator/tests.py
Normal file
90
examples/miniapps/ghnav-flask/githubnavigator/tests.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
"""Tests module."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from github import Github
|
||||
from flask import url_for
|
||||
|
||||
from .application import Application
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application():
|
||||
application = Application()
|
||||
application.config.from_dict(
|
||||
{
|
||||
'github': {
|
||||
'auth_token': 'test-token',
|
||||
'request_timeout': 10,
|
||||
},
|
||||
'search': {
|
||||
'default_term': 'Dependency Injector',
|
||||
'default_limit': 5,
|
||||
},
|
||||
}
|
||||
)
|
||||
return application
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def app(application: Application):
|
||||
return application.app()
|
||||
|
||||
|
||||
def test_index(client, application: Application):
|
||||
github_client_mock = mock.Mock(spec=Github)
|
||||
github_client_mock.search_repositories.return_value = [
|
||||
mock.Mock(
|
||||
html_url='repo1-url',
|
||||
name='repo1-name',
|
||||
owner=mock.Mock(
|
||||
login='owner1-login',
|
||||
html_url='owner1-url',
|
||||
avatar_url='owner1-avatar-url',
|
||||
),
|
||||
created_at='repo1-created-at',
|
||||
get_commits=mock.Mock(return_value=[mock.Mock()]),
|
||||
),
|
||||
mock.Mock(
|
||||
html_url='repo2-url',
|
||||
name='repo2-name',
|
||||
owner=mock.Mock(
|
||||
login='owner2-login',
|
||||
html_url='owner2-url',
|
||||
avatar_url='owner2-avatar-url',
|
||||
),
|
||||
created_at='repo2-created-at',
|
||||
get_commits=mock.Mock(return_value=[mock.Mock()]),
|
||||
),
|
||||
]
|
||||
|
||||
with application.github_client.override(github_client_mock):
|
||||
response = client.get(url_for('index'))
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
assert b'repo1-url' in response.data
|
||||
assert b'repo1-name' in response.data
|
||||
assert b'owner1-login' in response.data
|
||||
assert b'owner1-url' in response.data
|
||||
assert b'owner1-avatar-url' in response.data
|
||||
assert b'repo1-created-at' in response.data
|
||||
|
||||
assert b'repo2-url' in response.data
|
||||
assert b'repo2-name' in response.data
|
||||
assert b'owner2-login' in response.data
|
||||
assert b'owner2-url' in response.data
|
||||
assert b'owner2-avatar-url' in response.data
|
||||
assert b'repo2-created-at' in response.data
|
||||
|
||||
|
||||
def test_index_no_results(client, application: Application):
|
||||
github_client_mock = mock.Mock(spec=Github)
|
||||
github_client_mock.search_repositories.return_value = []
|
||||
|
||||
with application.github_client.override(github_client_mock):
|
||||
response = client.get(url_for('index'))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b'No search results' in response.data
|
19
examples/miniapps/ghnav-flask/githubnavigator/views.py
Normal file
19
examples/miniapps/ghnav-flask/githubnavigator/views.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
"""Views module."""
|
||||
|
||||
from flask import request, render_template
|
||||
|
||||
from .services import SearchService
|
||||
|
||||
|
||||
def index(search_service: SearchService, default_search_term, default_search_limit):
|
||||
search_term = request.args.get('search_term', default_search_term)
|
||||
limit = request.args.get('limit', default_search_limit, int)
|
||||
|
||||
repositories = search_service.search_repositories(search_term, limit)
|
||||
|
||||
return render_template(
|
||||
'index.html',
|
||||
search_term=search_term,
|
||||
limit=limit,
|
||||
repositories=repositories,
|
||||
)
|
4
examples/miniapps/ghnav-flask/requirements.txt
Normal file
4
examples/miniapps/ghnav-flask/requirements.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
dependency-injector[flask,yaml]
|
||||
pygithub
|
||||
pytest-flask
|
||||
pytest-cov
|
|
@ -6,3 +6,5 @@ flake8
|
|||
pydocstyle
|
||||
sphinx_autobuild
|
||||
pip
|
||||
|
||||
-r requirements-ext.txt
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
sphinx
|
||||
sphinx_rtd_theme>=0.2.5b2
|
||||
-e git://github.com/rmk135/sphinxcontrib-disqus.git#egg=sphinxcontrib-disqus
|
||||
|
||||
-r requirements-ext.txt
|
||||
|
|
1
requirements-ext.txt
Normal file
1
requirements-ext.txt
Normal file
|
@ -0,0 +1 @@
|
|||
flask
|
5
setup.py
5
setup.py
|
@ -41,6 +41,7 @@ setup(name='dependency-injector',
|
|||
download_url='https://pypi.python.org/pypi/dependency_injector',
|
||||
packages=[
|
||||
'dependency_injector',
|
||||
'dependency_injector.ext',
|
||||
],
|
||||
package_dir={
|
||||
'': 'src',
|
||||
|
@ -63,6 +64,9 @@ setup(name='dependency-injector',
|
|||
'yaml': [
|
||||
'pyyaml',
|
||||
],
|
||||
'flask': [
|
||||
'flask',
|
||||
],
|
||||
},
|
||||
zip_safe=True,
|
||||
license='BSD New',
|
||||
|
@ -75,6 +79,7 @@ setup(name='dependency-injector',
|
|||
'Factory',
|
||||
'Singleton',
|
||||
'Design patterns',
|
||||
'Flask',
|
||||
],
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Dependency injector top-level package."""
|
||||
|
||||
__version__ = '3.19.2'
|
||||
__version__ = '3.20.0'
|
||||
"""Version number that follows semantic versioning.
|
||||
|
||||
:type: str
|
||||
|
|
1
src/dependency_injector/ext/__init__.py
Normal file
1
src/dependency_injector/ext/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Extensions package."""
|
83
src/dependency_injector/ext/flask.py
Normal file
83
src/dependency_injector/ext/flask.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
"""Flask extension module."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from dependency_injector import providers, errors
|
||||
|
||||
|
||||
def create_app(name, routes, **kwargs):
|
||||
"""Create Flask app and add routes."""
|
||||
app = Flask(name, **kwargs)
|
||||
for route in routes:
|
||||
app.add_url_rule(*route.args, **route.options)
|
||||
return app
|
||||
|
||||
|
||||
def as_view(provider, name=None):
|
||||
"""Transform class-based view provider to view function."""
|
||||
if isinstance(provider, providers.Factory):
|
||||
def view(*args, **kwargs):
|
||||
self = provider()
|
||||
return self.dispatch_request(*args, **kwargs)
|
||||
|
||||
assert name, 'Argument "endpoint" is required for class-based views'
|
||||
view.__name__ = name
|
||||
elif isinstance(provider, providers.Callable):
|
||||
def view(*args, **kwargs):
|
||||
return provider(*args, **kwargs)
|
||||
|
||||
view.__name__ = provider.provides.__name__
|
||||
else:
|
||||
raise errors.Error('Undefined provider type')
|
||||
|
||||
view.__doc__ = provider.provides.__doc__
|
||||
view.__module__ = provider.provides.__module__
|
||||
|
||||
if isinstance(provider.provides, type):
|
||||
view.view_class = provider.provides
|
||||
|
||||
if hasattr(provider.provides, 'decorators'):
|
||||
for decorator in provider.provides.decorators:
|
||||
view = decorator(view)
|
||||
|
||||
if hasattr(provider.provides, 'methods'):
|
||||
view.methods = provider.provides.methods
|
||||
|
||||
if hasattr(provider.provides, 'provide_automatic_options'):
|
||||
view.provide_automatic_options = provider.provides.provide_automatic_options
|
||||
|
||||
return view
|
||||
|
||||
|
||||
class Route:
|
||||
"""Route is a glue for Dependency Injector providers and Flask views."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rule,
|
||||
endpoint=None,
|
||||
view_provider=None,
|
||||
provide_automatic_options=None,
|
||||
**options):
|
||||
"""Initialize route."""
|
||||
self.view_provider = view_provider
|
||||
self.args = (rule, endpoint, as_view(view_provider, endpoint), provide_automatic_options)
|
||||
self.options = options
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
"""Create and return full copy of provider."""
|
||||
copied = memo.get(id(self))
|
||||
if copied is not None:
|
||||
return copied
|
||||
|
||||
rule, endpoint, _, provide_automatic_options = self.args
|
||||
view_provider = providers.deepcopy(self.view_provider, memo)
|
||||
|
||||
return self.__class__(
|
||||
rule,
|
||||
endpoint,
|
||||
view_provider,
|
||||
provide_automatic_options,
|
||||
**self.options)
|
File diff suppressed because it is too large
Load Diff
|
@ -1405,6 +1405,11 @@ cdef class Factory(Provider):
|
|||
|
||||
@property
|
||||
def cls(self):
|
||||
"""Return provided type."""
|
||||
return self.provides
|
||||
|
||||
@property
|
||||
def provides(self):
|
||||
"""Return provided type."""
|
||||
return self.__instantiator.provides
|
||||
|
||||
|
|
1
tests/unit/ext/__init__.py
Normal file
1
tests/unit/ext/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Dependency injector extension unit tests."""
|
75
tests/unit/ext/test_flask_py2_py3.py
Normal file
75
tests/unit/ext/test_flask_py2_py3.py
Normal file
|
@ -0,0 +1,75 @@
|
|||
"""Dependency injector Flask extension unit tests."""
|
||||
|
||||
import unittest2 as unittest
|
||||
from flask import url_for
|
||||
from flask.views import MethodView
|
||||
|
||||
from dependency_injector import containers, providers
|
||||
from dependency_injector.ext import flask
|
||||
|
||||
|
||||
def index():
|
||||
return 'Hello World!'
|
||||
|
||||
|
||||
def test():
|
||||
return 'Test!'
|
||||
|
||||
|
||||
class Test(MethodView):
|
||||
def get(self):
|
||||
return 'Test class-based!'
|
||||
|
||||
|
||||
class Application(containers.DeclarativeContainer):
|
||||
|
||||
index_view = providers.Callable(index)
|
||||
test_view = providers.Callable(test)
|
||||
test_class_view = providers.Factory(Test)
|
||||
|
||||
app = providers.Factory(
|
||||
flask.create_app,
|
||||
name=__name__,
|
||||
routes=[
|
||||
flask.Route('/', view_provider=index_view),
|
||||
flask.Route('/test', 'test-test', test_view),
|
||||
flask.Route('/test-class', 'test-class', test_class_view)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class ApplicationTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
application = Application()
|
||||
self.app = application.app()
|
||||
self.app.config['SERVER_NAME'] = 'test-server.com'
|
||||
self.client = self.app.test_client()
|
||||
self.client.__enter__()
|
||||
|
||||
def tearDown(self):
|
||||
self.client.__exit__(None, None, None)
|
||||
|
||||
def test_index(self):
|
||||
response = self.client.get('/')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data, b'Hello World!')
|
||||
|
||||
def test_test(self):
|
||||
response = self.client.get('/test')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data, b'Test!')
|
||||
|
||||
def test_test_class_based(self):
|
||||
response = self.client.get('/test-class')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data, b'Test class-based!')
|
||||
|
||||
def test_endpoints(self):
|
||||
with self.app.app_context():
|
||||
self.assertEqual(url_for('index'), 'http://test-server.com/')
|
||||
self.assertEqual(url_for('test-test'), 'http://test-server.com/test')
|
||||
self.assertEqual(url_for('test-class'), 'http://test-server.com/test-class')
|
Loading…
Reference in New Issue
Block a user