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 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 http://www.martinfowler.com/articles/injection.html
Create virtual environment: Run
---
Create a virtual environment:
.. code-block:: bash .. code-block:: bash
virtualenv venv virtualenv venv
. venv/bin/activate . venv/bin/activate
Install requirements: Install the requirements:
.. code-block:: bash .. code-block:: bash
pip install -r requirements.txt pip install -r requirements.txt
To create the fixtures do:
.. code-block:: bash
python data/fixtures.py
To run the application do: To run the application do:
.. code-block:: bash .. code-block:: bash
MOVIE_STORAGE_TYPE=csv python -m movies MOVIE_FINDER_TYPE=csv python -m movies
MOVIE_STORAGE_TYPE=sqlite python -m movies MOVIE_FINDER_TYPE=sqlite python -m movies
The output should be something like: The output should be something like:
.. code-block:: bash .. code-block:: bash
[Movie(name='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] Francis Lawrence movies: [Movie(name='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')]
[Movie(name='The 33', year=2015, director='Patricia Riggen')] 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')]
[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')] Test
----
To run the tests do: To run the tests do:
@ -55,13 +65,11 @@ The output should be something like:
Name Stmts Miss Cover Name Stmts Miss Cover
------------------------------------------ ------------------------------------------
movies/__init__.py 0 0 100% 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/containers.py 9 0 100%
movies/finders.py 9 0 100% movies/entities.py 7 1 86%
movies/fixtures.py 1 0 100% movies/finders.py 26 13 50%
movies/listers.py 8 0 100% movies/listers.py 8 0 100%
movies/models.py 7 1 86%
movies/storages.py 32 17 47%
movies/tests.py 24 0 100% movies/tests.py 24 0 100%
------------------------------------------ ------------------------------------------
TOTAL 105 33 69% TOTAL 84 24 71%

View File

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

View File

@ -1,5 +1,6 @@
# Ignore everything in this directory # Everything
* *
# Except this file: # Except this file:
!.gitignore !.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(): def main():
container = ApplicationContainer() container = ApplicationContainer()
container.config.from_yaml('config.yml')
container.config.storage.type.from_env('MOVIE_STORAGE_TYPE')
storage = container.storage() container.config.from_yaml('config.yml')
fixtures = container.fixtures() container.config.finder.type.from_env('MOVIE_FINDER_TYPE')
storage.load_all(fixtures)
lister = container.lister() lister = container.lister()
print(lister.movies_directed_by('Francis Lawrence'))
print(lister.movies_directed_by('Patricia Riggen')) print(
print(lister.movies_directed_by('JJ Abrams')) 'Francis Lawrence movies:',
print(lister.movies_released_in(2015)) lister.movies_directed_by('Francis Lawrence'),
)
print(
'2016 movies:',
lister.movies_released_in(2016),
)
if __name__ == '__main__': if __name__ == '__main__':

View File

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

View File

@ -1,23 +1,50 @@
"""Movie finders module.""" """Movie finders module."""
import csv
import sqlite3
from typing import Callable, List from typing import Callable, List
from .models import Movie from .entities import Movie
from .storages import MovieStorage
class MovieFinder: 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__( def __init__(
self, self,
movie_factory: Callable[..., Movie], movie_factory: Callable[..., Movie],
movie_storage: MovieStorage, path: str,
delimiter: str,
) -> None: ) -> None:
self._movie_factory = movie_factory self._csv_file_path = path
self._movie_storage = movie_storage self._delimiter = delimiter
super().__init__(movie_factory)
def find_all(self) -> List[Movie]: def find_all(self) -> List[Movie]:
return [ with open(self._csv_file_path) as csv_file:
self._movie_factory(*row) csv_reader = csv.reader(csv_file, delimiter=self._delimiter)
for row in self._movie_storage.get_all() 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(): def container():
container = ApplicationContainer() container = ApplicationContainer()
container.config.from_dict({ container.config.from_dict({
'storage': { 'finder': {
'type': 'csv', 'type': 'csv',
'csv': { 'csv': {
'path': '/fake-movies.csv', 'path': '/fake-movies.csv',
@ -26,13 +26,13 @@ def container():
def test_movies_directed_by(container): def test_movies_directed_by(container):
storage_mock = mock.Mock() finder_mock = mock.Mock()
storage_mock.get_all.return_value = [ finder_mock.find_all.return_value = [
('The 33', 2015, 'Patricia Riggen'), container.movie('The 33', 2015, 'Patricia Riggen'),
('The Jungle Book', 2016, 'Jon Favreau'), container.movie('The Jungle Book', 2016, 'Jon Favreau'),
] ]
with container.storage.override(storage_mock): with container.finder.override(finder_mock):
lister = container.lister() lister = container.lister()
movies = lister.movies_directed_by('Jon Favreau') movies = lister.movies_directed_by('Jon Favreau')
@ -41,13 +41,13 @@ def test_movies_directed_by(container):
def test_movies_released_in(container): def test_movies_released_in(container):
storage_mock = mock.Mock() finder_mock = mock.Mock()
storage_mock.get_all.return_value = [ finder_mock.find_all.return_value = [
('The 33', 2015, 'Patricia Riggen'), container.movie('The 33', 2015, 'Patricia Riggen'),
('The Jungle Book', 2016, 'Jon Favreau'), container.movie('The Jungle Book', 2016, 'Jon Favreau'),
] ]
with container.storage.override(storage_mock): with container.finder.override(finder_mock):
lister = container.lister() lister = container.lister()
movies = lister.movies_released_in(2015) movies = lister.movies_released_in(2015)