Make second round of the refactoring

This commit is contained in:
Roman Mogylatov 2020-08-11 21:51:20 -04:00
parent d5fd46b159
commit 59ca0f8ace
11 changed files with 143 additions and 126 deletions

View File

@ -2,38 +2,48 @@ Movie lister - a naive example of dependency injection in Python
================================================================
This is a Python implementation of the dependency injection example from Martin Fowler's
article about dependency injection and inversion of control:
article:
http://www.martinfowler.com/articles/injection.html
Create virtual environment:
Run
---
Create a virtual environment:
.. code-block:: bash
virtualenv venv
. venv/bin/activate
Install requirements:
Install the requirements:
.. code-block:: bash
pip install -r requirements.txt
To create the fixtures do:
.. code-block:: bash
python data/fixtures.py
To run the application do:
.. code-block:: bash
MOVIE_STORAGE_TYPE=csv python -m movies
MOVIE_STORAGE_TYPE=sqlite python -m movies
MOVIE_FINDER_TYPE=csv python -m movies
MOVIE_FINDER_TYPE=sqlite python -m movies
The output should be something like:
.. code-block:: bash
[Movie(name='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
[Movie(name='The 33', year=2015, director='Patricia Riggen')]
[Movie(name='Star Wars: Episode VII - The Force Awakens', year=2015, director='JJ Abrams')]
[Movie(name='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence'), Movie(name='The 33', year=2015, director='Patricia Riggen'), Movie(name='Star Wars: Episode VII - The Force Awakens', year=2015, director='JJ Abrams')]
Francis Lawrence movies: [Movie(name='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
2016 movies: [Movie(name='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(name='The Jungle Book', year=2016, director='Jon Favreau')]
Test
----
To run the tests do:
@ -55,13 +65,11 @@ The output should be something like:
Name Stmts Miss Cover
------------------------------------------
movies/__init__.py 0 0 100%
movies/__main__.py 15 15 0%
movies/__main__.py 10 10 0%
movies/containers.py 9 0 100%
movies/finders.py 9 0 100%
movies/fixtures.py 1 0 100%
movies/entities.py 7 1 86%
movies/finders.py 26 13 50%
movies/listers.py 8 0 100%
movies/models.py 7 1 86%
movies/storages.py 32 17 47%
movies/tests.py 24 0 100%
------------------------------------------
TOTAL 105 33 69%
TOTAL 84 24 71%

View File

@ -1,4 +1,4 @@
storage:
finder:
csv:
path: "data/movies.csv"

View File

@ -1,5 +1,6 @@
# Ignore everything in this directory
# Everything
*
# Except this file:
!.gitignore
!fixtures.py

View File

@ -0,0 +1,43 @@
"""Fixtures module."""
import csv
import sqlite3
import pathlib
SAMPLE_DATA = [
('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'),
('Rogue One: A Star Wars Story', 2016, 'Gareth Edwards'),
('The Jungle Book', 2016, 'Jon Favreau'),
]
FILE = pathlib.Path(__file__)
DIR = FILE.parent
CSV_FILE = DIR / 'movies.csv'
SQLITE_FILE = DIR / 'movies.db'
def create_csv(movies_data, path):
with open(path, 'w') as opened_file:
writer = csv.writer(opened_file)
for row in movies_data:
writer.writerow(row)
def create_sqlite(movies_data, path):
with sqlite3.connect(path) as db:
db.execute(
'CREATE TABLE IF NOT EXISTS movies '
'(name text, year int, director text)'
)
db.execute('DELETE FROM movies')
db.executemany('INSERT INTO movies VALUES (?,?,?)', movies_data)
def main():
create_csv(SAMPLE_DATA, CSV_FILE)
create_sqlite(SAMPLE_DATA, SQLITE_FILE)
if __name__ == '__main__':
main()

View File

@ -5,18 +5,20 @@ from .containers import ApplicationContainer
def main():
container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.config.storage.type.from_env('MOVIE_STORAGE_TYPE')
storage = container.storage()
fixtures = container.fixtures()
storage.load_all(fixtures)
container.config.from_yaml('config.yml')
container.config.finder.type.from_env('MOVIE_FINDER_TYPE')
lister = container.lister()
print(lister.movies_directed_by('Francis Lawrence'))
print(lister.movies_directed_by('Patricia Riggen'))
print(lister.movies_directed_by('JJ Abrams'))
print(lister.movies_released_in(2015))
print(
'Francis Lawrence movies:',
lister.movies_directed_by('Francis Lawrence'),
)
print(
'2016 movies:',
lister.movies_released_in(2016),
)
if __name__ == '__main__':

View File

@ -2,33 +2,32 @@
from dependency_injector import containers, providers
from . import finders, listers, storages, models, fixtures
from . import finders, listers, entities
class ApplicationContainer(containers.DeclarativeContainer):
config = providers.Configuration()
fixtures = providers.Object(fixtures.MOVIES_SAMPLE_DATA)
movie = providers.Factory(entities.Movie)
storage = providers.Selector(
config.storage.type,
csv=providers.Singleton(
storages.CsvMovieStorage,
options=config.storage[config.storage.type],
),
sqlite=providers.Singleton(
storages.SqliteMovieStorage,
options=config.storage[config.storage.type],
),
csv_finder = providers.Singleton(
finders.CsvMovieFinder,
movie_factory=movie.provider,
path=config.finder.csv.path,
delimiter=config.finder.csv.delimiter,
)
movie = providers.Factory(models.Movie)
finder = providers.Factory(
finders.MovieFinder,
sqlite_finder = providers.Singleton(
finders.SqliteMovieFinder,
movie_factory=movie.provider,
movie_storage=storage,
path=config.finder.sqlite.path,
)
finder = providers.Selector(
config.finder.type,
csv=csv_finder,
sqlite=sqlite_finder,
)
lister = providers.Factory(

View File

@ -1,23 +1,50 @@
"""Movie finders module."""
import csv
import sqlite3
from typing import Callable, List
from .models import Movie
from .storages import MovieStorage
from .entities import Movie
class MovieFinder:
def __init__(self, movie_factory: Callable[..., Movie]) -> None:
self._movie_factory = movie_factory
def find_all(self) -> List[Movie]:
raise NotImplementedError()
class CsvMovieFinder(MovieFinder):
def __init__(
self,
movie_factory: Callable[..., Movie],
movie_storage: MovieStorage,
path: str,
delimiter: str,
) -> None:
self._movie_factory = movie_factory
self._movie_storage = movie_storage
self._csv_file_path = path
self._delimiter = delimiter
super().__init__(movie_factory)
def find_all(self) -> List[Movie]:
return [
self._movie_factory(*row)
for row in self._movie_storage.get_all()
]
with open(self._csv_file_path) as csv_file:
csv_reader = csv.reader(csv_file, delimiter=self._delimiter)
return [self._movie_factory(*row) for row in csv_reader]
class SqliteMovieFinder(MovieFinder):
def __init__(
self,
movie_factory: Callable[..., Movie],
path: str,
) -> None:
self._database = sqlite3.connect(path)
super().__init__(movie_factory)
def find_all(self) -> List[Movie]:
with self._database as db:
rows = db.execute('SELECT name, year, director FROM movies')
return [self._movie_factory(*row) for row in rows]

View File

@ -1,8 +0,0 @@
"""Fixtures module."""
MOVIES_SAMPLE_DATA = [
('The Hunger Games: Mockingjay - Part 2', 2015, 'Francis Lawrence'),
('The 33', 2015, 'Patricia Riggen'),
('Star Wars: Episode VII - The Force Awakens', 2015, 'JJ Abrams'),
]

View File

@ -1,55 +0,0 @@
"""Movie storages module."""
import csv
import sqlite3
from typing import List, Tuple, Any
Row = Tuple[Any]
class MovieStorage:
def load_all(self, movie_data: List[Row]):
raise NotImplementedError()
def get_all(self) -> List[Row]:
raise NotImplementedError()
class CsvMovieStorage(MovieStorage):
def __init__(self, options) -> None:
self._csv_file_path = options.pop('path')
self._delimiter = options.pop('delimiter')
def load_all(self, movie_data: List[Row]) -> None:
with open(self._csv_file_path, 'w') as csv_file:
csv.writer(csv_file, delimiter=self._delimiter).writerows(movie_data)
def get_all(self) -> List[Row]:
with open(self._csv_file_path) as csv_file:
csv_reader = csv.reader(csv_file, delimiter=self._delimiter)
return [row for row in csv_reader]
class SqliteMovieStorage(MovieStorage):
def __init__(self, options) -> None:
self._database = sqlite3.connect(database=options.pop('path'))
def load_all(self, movie_data: List[Row]) -> None:
with self._database as db:
db.execute(
'CREATE TABLE IF NOT EXISTS movies '
'(name text, year int, director text)',
)
db.execute('DELETE FROM movies')
db.executemany('INSERT INTO movies VALUES (?,?,?)', movie_data)
def get_all(self) -> List[Row]:
with self._database as db:
rows = db.execute(
'SELECT name, year, director '
'FROM movies',
)
return [row for row in rows]

View File

@ -11,7 +11,7 @@ from .containers import ApplicationContainer
def container():
container = ApplicationContainer()
container.config.from_dict({
'storage': {
'finder': {
'type': 'csv',
'csv': {
'path': '/fake-movies.csv',
@ -26,13 +26,13 @@ def container():
def test_movies_directed_by(container):
storage_mock = mock.Mock()
storage_mock.get_all.return_value = [
('The 33', 2015, 'Patricia Riggen'),
('The Jungle Book', 2016, 'Jon Favreau'),
finder_mock = mock.Mock()
finder_mock.find_all.return_value = [
container.movie('The 33', 2015, 'Patricia Riggen'),
container.movie('The Jungle Book', 2016, 'Jon Favreau'),
]
with container.storage.override(storage_mock):
with container.finder.override(finder_mock):
lister = container.lister()
movies = lister.movies_directed_by('Jon Favreau')
@ -41,13 +41,13 @@ def test_movies_directed_by(container):
def test_movies_released_in(container):
storage_mock = mock.Mock()
storage_mock.get_all.return_value = [
('The 33', 2015, 'Patricia Riggen'),
('The Jungle Book', 2016, 'Jon Favreau'),
finder_mock = mock.Mock()
finder_mock.find_all.return_value = [
container.movie('The 33', 2015, 'Patricia Riggen'),
container.movie('The Jungle Book', 2016, 'Jon Favreau'),
]
with container.storage.override(storage_mock):
with container.finder.override(finder_mock):
lister = container.lister()
movies = lister.movies_released_in(2015)