From 8e8f266fdfde8639698a6a50691403ff6aec807e Mon Sep 17 00:00:00 2001 From: Roman Mogylatov Date: Wed, 12 Aug 2020 16:34:50 -0400 Subject: [PATCH] Add CLI app tutorial --- docs/providers/selector.rst | 2 + docs/tutorials/cli-images/classes_01.png | Bin 0 -> 37176 bytes docs/tutorials/cli-images/classes_02.png | Bin 0 -> 38614 bytes docs/tutorials/cli.rst | 1053 ++++++++++++++++++++++ docs/tutorials/index.rst | 1 + 5 files changed, 1056 insertions(+) create mode 100644 docs/tutorials/cli-images/classes_01.png create mode 100644 docs/tutorials/cli-images/classes_02.png create mode 100644 docs/tutorials/cli.rst diff --git a/docs/providers/selector.rst b/docs/providers/selector.rst index 883e4fa0..29867217 100644 --- a/docs/providers/selector.rst +++ b/docs/providers/selector.rst @@ -1,3 +1,5 @@ +.. _selector-provider: + Selector providers ------------------ diff --git a/docs/tutorials/cli-images/classes_01.png b/docs/tutorials/cli-images/classes_01.png new file mode 100644 index 0000000000000000000000000000000000000000..ee4575a5d294e7dbda28e94cd4acba4ee4d818f6 GIT binary patch literal 37176 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 literal 0 HcmV?d00001 diff --git a/docs/tutorials/cli-images/classes_02.png b/docs/tutorials/cli-images/classes_02.png new file mode 100644 index 0000000000000000000000000000000000000000..f768d1877431ef84e6c80fff357bf772260ca8bd GIT binary patch literal 38614 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 YYp?(OhMg2!3`4kT%GyfU+ZI3n4`_. + +What are we going to build? +--------------------------- + +We will build a CLI application that helps to search for the movies. Let's call it Movie Lister. + +How does Movie Lister work? + +- Application uses a movies database to search for the movies +- Application can search the movies by: + - Director's name + - Year of the release +- Each movie has next fields: + - Title + - Year of the release + - Director's name +- The database can be in the next formats: + - Csv + - Sqlite +- Other database formats can be added later + +Movie Lister is a naive example from Martin Fowler's article about the dependency injection and +inversion of control: + + http://www.martinfowler.com/articles/injection.html + +Here is a class diagram of the Movie Lister application: + +.. image:: cli-images/classes_01.png + +The responsibilities are split that way: + +- ``MovieLister`` - is responsible for the search +- ``MovieFinder`` - is responsible for the fetching from the database +- ``Movie`` - the movie entity + +Prepare the environment +----------------------- + +Let's create the environment for the project. + +First we need to create a project folder and the virtual environment: + +.. code-block:: bash + + mkdir movie-lister-tutorial + cd movie-lister-tutorial + python3 -m venv venv + +Now let's activate the virtual environment: + +.. code-block:: bash + + . venv/bin/activate + +Project layout +-------------- + +Create next structure in the project root directory. All files are empty. That's ok for now. + +Initial project layout: + +.. code-block:: bash + + ./ + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ └── containers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +Move on to the project requirements. + +Install the requirements +------------------------ + +Now it's time to install the project requirements. We will use next packages: + +- ``dependency-injector`` - the dependency injection framework +- ``pyyaml`` - the YAML files parsing library, used for the reading of the configuration files +- ``pytest`` - the test framework +- ``pytest-cov`` - the helper library for measuring the test coverage + +Put next lines into the ``requirements.txt`` file: + +.. code-block:: bash + + dependency-injector + pyyaml + pytest + pytest-cov + +and run next in the terminal: + +.. code-block:: bash + + pip install -r requirements.txt + +The requirements are setup. Now we will add the fixtures. + +Fixtures +-------- + +In this section we will add the fixtures. + +We will create a script that creates database files. + +First add the folder ``data/`` in the root of the project and then add the file +``fixtures.py`` inside of it: + +.. code-block:: bash + :emphasize-lines: 2-3 + + ./ + ├── data/ + │ └── fixtures.py + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ └── containers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +Second put next in the ``fixtures.py``: + +.. code-block:: python + + """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 ' + '(title 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) + print('OK') + + + if __name__ == '__main__': + main() + +Now run in the terminal: + +.. code-block:: bash + + python data/fixtures.py + +You should see: + +.. code-block:: bash + + OK + +Check that files ``movies.csv`` and ``movies.db`` have appeared in the ``data/`` folder: + +.. code-block:: bash + :emphasize-lines: 4-5 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ └── containers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +Fixtures are created. Let's move on. + +Container +--------- + +In this section we will add the main part of our application - the container. + +Container will keep all of the application components and their dependencies. + +Edit ``containers.py``: + +.. code-block:: python + + """Containers module.""" + + from dependency_injector import containers + + + class ApplicationContainer(containers.DeclarativeContainer): + ... + +Container is empty for now. We will add the providers in the following sections. + +Let's also create the ``main()`` function. Its responsibility is to run our application. For now +it will just create the container. + +Edit ``__main__.py``: + +.. code-block:: python + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + + if __name__ == '__main__': + main() + +.. note:: + + Container is the first object in the application. + + The container is used to create all other objects. + +Csv finder +---------- + +In this section we will build everything we need for working with the csv file formats. + +We will add: + +- The ``Movie`` entity +- The ``MovieFinder`` base class +- The ``CsvMovieFinder`` finder implementation +- The ``MovieLister`` class + +After each step we will add the provider to the container. + +.. image:: cli-images/classes_02.png + +Create the ``entities.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 10 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ └── entities.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + + """Movie entities module.""" + + + class Movie: + + def __init__(self, title: str, year: int, director: str): + self.title = str(title) + self.year = int(year) + self.director = str(director) + + def __repr__(self): + return '{0}(title={1}, year={2}, director={3})'.format( + self.__class__.__name__, + repr(self.title), + repr(self.year), + repr(self.director), + ) + +Now we need to add the ``Movie`` factory to the container. We need to add import of the +``providers`` module from the ``dependency_injector`` package, import ``entities`` module. + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 3,5,9 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import entities + + class ApplicationContainer(containers.DeclarativeContainer): + + movie = providers.Factory(entities.Movie) + +.. note:: + + Don't forget to remove the Ellipsis ``...`` from the container. We don't need it anymore + since we container is not empty. + +Let's move on to the finders. + +Create the ``finders.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 11 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ ├── entities.py + │ └── finders.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + + """Movie finders module.""" + + import csv + from typing import Callable, List + + 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], + path: str, + delimiter: str, + ) -> None: + self._csv_file_path = path + self._delimiter = delimiter + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + 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] + +Now let's add the csv finder into the container. + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 5,9,13-18 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, entities + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + +The csv finder needs the movie factory. It needs it to create the ``Movie`` entities when +reads the csv rows. To provide the factory we use ``.provider`` factory attribute. +This is also called the delegation of the provider. If we just pass the movie factory +as the dependency, it will be called when csv finder is created and the ``Movie`` instance will +be injected. With the ``.provider`` attribute the provider itself will be injected. + +The csv finder also has a few dependencies on the configuration options. We added configuration +provider to provide these dependencies. + +.. note:: + + We have used the configuration value before it was defined. That's the principle how the + Configuration provider works. + + Use first, define later. + +Not let's define the configuration values. + +Edit ``config.yml``: + +.. code-block:: yaml + + finder: + + csv: + path: "data/movies.csv" + delimiter: "," + +The configuration file is ready. Now let's update the ``main()`` function to specify its location. + +Edit ``__main__.py``: + +.. code-block:: python + :emphasize-lines: 9 + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + + + if __name__ == '__main__': + main() + +Move on to the lister. + +Create the ``listers.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 12 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ ├── entities.py + │ ├── finders.py + │ └── listers.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + + """Movie listers module.""" + + from .finders import MovieFinder + + + class MovieLister: + + def __init__(self, movie_finder: MovieFinder): + self._movie_finder = movie_finder + + def movies_directed_by(self, director): + return [ + movie for movie in self._movie_finder.find_all() + if movie.director == director + ] + + def movies_released_in(self, year): + return [ + movie for movie in self._movie_finder.find_all() + if movie.year == year + ] + +and edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 5,20-23 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=csv_finder, + ) + +All the components are created and added to the container. + +Finally let's update the ``main()`` function. + +Edit ``__main__.py``: + +.. code-block:: python + :emphasize-lines: 11-20 + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + + lister = container.lister() + + print( + 'Francis Lawrence movies:', + lister.movies_directed_by('Francis Lawrence'), + ) + print( + '2016 movies:', + lister.movies_released_in(2016), + ) + + + if __name__ == '__main__': + main() + +All set. Now we run the application. + +Run in the terminal: + +.. code-block:: bash + + python -m movies + +You should see: + +.. code-block:: bash + + Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] + 2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')] + +Our application can work with the movies database in the csv format. We also need to support +the sqlite format. We will deal with it in the next section. + +Sqlite finder +------------- + +In this section we will add another type of the finder - the sqlite finder. + +Let's get to work. + +Edit ``finders.py``: + +.. code-block:: python + :emphasize-lines: 4,37-50 + + """Movie finders module.""" + + import csv + import sqlite3 + from typing import Callable, List + + 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], + path: str, + delimiter: str, + ) -> None: + self._csv_file_path = path + self._delimiter = delimiter + super().__init__(movie_factory) + + def find_all(self) -> List[Movie]: + 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 title, year, director FROM movies') + return [self._movie_factory(*row) for row in rows] + +Now we need to add the sqlite finder to the container and update lister's dependency to use it. + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 20-24,28 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + sqlite_finder = providers.Singleton( + finders.SqliteMovieFinder, + movie_factory=movie.provider, + path=config.finder.sqlite.path, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=sqlite_finder, + ) + +The sqlite finder has a dependency on the configuration option. Let's update the configuration +file. + +Edit ``config.yml``: + +.. code-block:: yaml + :emphasize-lines: 7-8 + + finder: + + csv: + path: "data/movies.csv" + delimiter: "," + + sqlite: + path: "data/movies.db" + +All is ready. Let's check. + +Run in the terminal: + +.. code-block:: bash + + python -m movies + +You should see: + +.. code-block:: bash + + Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] + 2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')] + +Our application now supports both formats: csv files and sqlite databases. Every time when we +need to work with the different format we need to make a code change in the container. We will +improve this in the next section. + +Selector +-------- + +In this section we will make our application more flexible. + +The code change will not be needed to switch between csv and sqlite formats. We implement the +switch based on the environment variable ``MOVIE_FINDER_TYPE``: + +- When ``MOVIE_FINDER_TYPE=csv`` application uses csv finder. +- When ``MOVIE_FINDER_TYPE=sqlite`` application uses sqlite finder. + +We will use the ``Selector`` provider. It selects the provider based on the configuration option +(docs - :ref:`selector-provider`). + +Edit ``containers.py``: + +.. code-block:: python + :emphasize-lines: 27-31,35 + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + sqlite_finder = providers.Singleton( + finders.SqliteMovieFinder, + movie_factory=movie.provider, + path=config.finder.sqlite.path, + ) + + finder = providers.Selector( + config.finder.type, + csv=csv_finder, + sqlite=sqlite_finder, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=finder, + ) + +The switch is the ``config.finder.type`` option. When its value is ``csv``, the provider under +``csv`` key is used. The same is for ``sqlite``. + +Now we need to read the value of the ``config.finder.type`` option from the environment variable +``MOVIE_FINDER_TYPE``. + +Edit ``__main__.py``: + +.. code-block:: python + :emphasize-lines: 10 + + """Main module.""" + + from .containers import ApplicationContainer + + + def main(): + container = ApplicationContainer() + + container.config.from_yaml('config.yml') + container.config.finder.type.from_env('MOVIE_FINDER_TYPE') + + lister = container.lister() + + print( + 'Francis Lawrence movies:', + lister.movies_directed_by('Francis Lawrence'), + ) + print( + '2016 movies:', + lister.movies_released_in(2016), + ) + + + if __name__ == '__main__': + main() + +Done. + +Run in the terminal line by line: + +.. code-block:: bash + + MOVIE_FINDER_TYPE=csv python -m movies + MOVIE_FINDER_TYPE=sqlite python -m movies + +The output should be something like this for each command: + +.. code-block:: bash + + Francis Lawrence movies: [Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')] + 2016 movies: [Movie(title='Rogue One: A Star Wars Story', year=2016, director='Gareth Edwards'), Movie(title='The Jungle Book', year=2016, director='Jon Favreau')] + +In the next section we will add some tests. + +Tests +----- + +It would be nice to add some tests. Let's do it. + +We will use `pytest `_ and +`coverage `_. + +Create ``tests.py`` in the ``movies`` package: + +.. code-block:: bash + :emphasize-lines: 13 + + ./ + ├── data/ + │ ├── fixtures.py + │ ├── movies.csv + │ └── movies.db + ├── movies/ + │ ├── __init__.py + │ ├── __main__.py + │ ├── containers.py + │ ├── entities.py + │ ├── finders.py + │ ├── listers.py + │ └── tests.py + ├── venv/ + ├── config.yml + └── requirements.txt + +and put next into it: + +.. code-block:: python + :emphasize-lines: 35,50 + + """Tests module.""" + + from unittest import mock + + import pytest + + from .containers import ApplicationContainer + + + @pytest.fixture + def container(): + container = ApplicationContainer() + container.config.from_dict({ + 'finder': { + 'type': 'csv', + 'csv': { + 'path': '/fake-movies.csv', + 'delimiter': ',', + }, + 'sqlite': { + 'path': '/fake-movies.db', + }, + }, + }) + return container + + + def test_movies_directed_by(container): + 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.finder.override(finder_mock): + lister = container.lister() + movies = lister.movies_directed_by('Jon Favreau') + + assert len(movies) == 1 + assert movies[0].title == 'The Jungle Book' + + + def test_movies_released_in(container): + 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.finder.override(finder_mock): + lister = container.lister() + movies = lister.movies_released_in(2015) + + assert len(movies) == 1 + assert movies[0].title == 'The 33' + +Run in the terminal: + +.. code-block:: bash + + pytest movies/tests.py --cov=movies + +You should see: + +.. code-block:: bash + + platform darwin -- Python 3.8.3, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 + plugins: cov-2.10.0 + collected 2 items + + movies/tests.py .. [100%] + + ---------- coverage: platform darwin, python 3.8.3-final-0 ----------- + Name Stmts Miss Cover + ------------------------------------------ + movies/__init__.py 0 0 100% + movies/__main__.py 10 10 0% + movies/containers.py 9 0 100% + movies/entities.py 7 1 86% + movies/finders.py 26 13 50% + movies/listers.py 8 0 100% + movies/tests.py 24 0 100% + ------------------------------------------ + TOTAL 84 24 71% + +.. note:: + + Take a look at the highlights in the ``tests.py``. + + We use ``.override()`` method of the ``finder`` provider. Provider is overridden by the mock. + Every time when any other provider will request ``finder`` provider to provide the dependency, + the mock will be returned. So when we call the ``lister`` provider, the ``MovieLister`` + instance is created with the mock, not an actual ``MovieFinder``. + +Conclusion +---------- + +In this tutorial we've built a CLI application following the dependency injection principle. +We've used the ``Dependency Injector`` as a dependency injection framework. + +The benefit you get with the ``Dependency Injector`` is the container. It starts to payoff +when you need to understand or change your application structure. It's easy with the container, +cause you have everything defined explicitly in one place: + +.. code-block:: python + + """Containers module.""" + + from dependency_injector import containers, providers + + from . import finders, listers, entities + + + class ApplicationContainer(containers.DeclarativeContainer): + + config = providers.Configuration() + + movie = providers.Factory(entities.Movie) + + csv_finder = providers.Singleton( + finders.CsvMovieFinder, + movie_factory=movie.provider, + path=config.finder.csv.path, + delimiter=config.finder.csv.delimiter, + ) + + sqlite_finder = providers.Singleton( + finders.SqliteMovieFinder, + movie_factory=movie.provider, + path=config.finder.sqlite.path, + ) + + finder = providers.Selector( + config.finder.type, + csv=csv_finder, + sqlite=sqlite_finder, + ) + + lister = providers.Factory( + listers.MovieLister, + movie_finder=finder, + ) + +What's next? + +- Look at the other :ref:`tutorials`. +- Know more about the :ref:`providers`. +- Go to the :ref:`contents`. + +.. disqus:: diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst index bfe6a2c2..f686fe4f 100644 --- a/docs/tutorials/index.rst +++ b/docs/tutorials/index.rst @@ -12,5 +12,6 @@ frameworks. flask aiohttp asyncio-daemon + cli .. disqus::