<style> .present { text-align: left; } img[alt=knight_moves] { width: 400px; } </style> # Python dependency management and unit-testing ## Week 18 Day 1 --- ## Lecture videos 1 (30min) Watch: - Local Packages and Modules - pip, virtualenv, and pipenv --- ### Python Modules A module is code that is imported from a file or directory. A module can be: 1. Built-in: already in Python's standard library 2. Third-party: downloaded via command line 3. Custom: your own code To import code from a module, we use the `import` keyword. The import keyword will locate and initialize a module, and give you access to the specific names you have imported in the file. --- ### The `import` keyword The Python standard library has a number of packages you can import without having to install them—they are included when you install Python ([documentation](https://docs.python.org/3/tutorial/stdlib.html)). Let's use the random package as an example (this would work the same with any package). ```python= import random # import everything from random print(random.randint(0, 10)) ``` With aliasing ```python= import random as rand # import everything, alias random as rand print(rand.randint(0, 10)) ``` --- ### The `import` keyword You can also import just specific functions from a package using the `from` keyword. ```python= from random import randint # import just the randint function print(randint(0, 10)) ``` ```python= from random import randint, shuffle # import multiple functions at the same time print(randint(0, 10)) ``` ```python= from random import randint as r_i, shuffle print(r_i(0, 10)) ``` --- ### Import Python code from a file If I have two files at the same level, I can import one file using the filename (minus the `.py`). ``` project_folder | my_code.py | other_code.py ``` ```python= # inside my_code.py import other_code # import just a specific item from other_code import my_function ``` When I import the `other_code.py` file, all of the code in that file will run, even if I'm just importing one function. --- ### Import Python code from a subdirectory ``` project_folder | my_code.py | other_code.py | subfolder | | file_one.py ``` To import code from inside a subfolder, use `import folder_name.file_name`. ```python= # inside my_code.py import subfolder.file_one # or from subfolder import file_one # or from subfolder.file_one import my_function ``` --- ### JavaScript imports Reminder: In Javascript, when we imported from other files, we used relative import statements. ``` project_folder | top_level_file.js └──subfolder | | file_one.js | | file_two.js ``` The import path changes depending on what file we are in. ```javascript= // inside top_level_file.js import { someObject } from "./subfolder/file_two" ``` ```javascript= // inside file_one.js import { someObject } from "./file_two" ``` --- ### Python imports ``` project_folder | top_level_file.py └──subfolder | | __init__.py | | file_one.py | | file_two.py ``` In Python, when you are importing code from other files, we (usually) use absolute import statements, which are relative to the top-level file being executed. ```python= # inside top_level_file.py import subfolder.file_two ``` ```python= # inside file_one.py import subfolder.file_two ``` However... --- ### Python Imports ...that means that if I try to run a file directly, instead of from the intended entrypoint of my application, the file won't work correctly. ```python= # inside top_level_file.py import subfolder.file_one print("Hello from top_level_file.py") ``` ```python= # inside file_one.py import subfolder.file_two print("Hello from file_one.py") ``` ```python= # inside file_two.py print("Hello from file_two.py") ``` --- ### Python Imports If you run the `top_level_file.py` file at the command line, you will get the following output: ``` Hello from file_two.py Hello from file_one.py Hello from top_level_file.py ``` If you run the `file_one.py` file at the command line, you will get the following output: ``` ModuleNotFoundError: No module named 'subfolder' ``` If I rewrote `file_one.py` so that its import statement worked when it runs in isolation (`import file_two`), then it wouldn't work in the context of the application. --- ### Relative imports in Python ``` project_folder | top_level_file.py └──subfolder | | __init__.py | | file_one.py | | file_two.py ``` ```python= # inside top_level_file.py from subfolder.file_two import some_variable ``` ```python= # inside file_one.py from .file_two import some_variable ``` Note that the relative syntax (`.file_two`) and the absolute syntax (`my_module.file_two`) will both be valid if you are importing `file_one` into the `top_level_file.py`—neither would work if you tried to run `file_one.py` directly. --- ### Python imports (takeaways) 1. Include a `__init__.py` file in any folder that has python code if you are going to be importing from that folder. The `__init__.py` file can be completely empty (and often will be). 2. Write all of your import statements relative to the top level of your project. 3. If you want to debug, don't just try to run a file outside of the context of the application. (That usually would be a bad idea anyway, even if the import statements worked differently.) 4. Play around with `import` statements if you want to learn more. --- ### Pip, Virtualenv, and Pipenv - **pyenv**: version manager for Python - **pip**: package manager for Python (but only works for globally installing packages) - **virtualenv**: the environment containing a specified python version and a collection of installed packages(in a `.venv` folder) - **pipenv**: dependency manager for individual projects --- ### Pip, Virtualenv, and Pipenv | Python tool | Node.js equivalent | |:------------- |:----------------------- | | pyenv | nvm | | pip | npm --global | | virtualenv | nvm + node_modules | | pipenv | npm + nvm | | Pipfile | package.json | | Pipfile.lock | package-lock.json | --- ### Using `pipenv` Create a virtual environment by running `pipenv install`. If there is a Pipfile present, this will install the dependencies in the Pipfile, otherwise it will create a new Pipfile along with a virtual environment. You can specify a particular version of Python to use in your virtual environment with `--python` flag. ```bash pipenv install --python 3.9.4 ``` You can also pass in a path instead of a number . ```bash pipenv install --python "/Users/username/.pyenv/versions/3.9.4/bin/python" ``` --- ### Specifying a Python version (note for projects this week) Many of the projects this week will specify a version of Python to use. If you try to use a version that you don't have installed, it will not work. Also, these projects expect you to be specifying the path instead of just a number. If you see something like this ```bash pipenv install --python "$PYENV_ROOT/versions/3.8.3/bin/python" ``` Run this instead: ```bash pipenv install --python 3.9.4 # or whatever version you do have installed ``` If you aren't sure, you can check to see which version you have available with the command `pyenv versions`. --- ### Installing packages with `pipenv` Install a dependency: ```bash pipenv install package-name ``` Install a development-only dependency: ```bash pipenv install --dev package-name ``` Uninstall a dependency: ```bash pipenv uninstall package-name ``` --- ### More `pipenv` commands Activate your virtual environment shell: ```bash pipenv shell ``` Remove a virtual environment: ```bash pipenv --rm ``` --- ## Lecture videos 2 (22 min) Watch: - Writing Unit Tests With unittest - Writing Unit Tests With pytest --- ### The `unittest` package To run tests with unittest: ```bash python -m unittest ``` All tests must be in a folder called `test` at the top level of the project. The `test` folder must contain a `__init__.py` --- ### Writing `unittest` tests Inside a test file: - import `unittest`. - import the code that we are testing. - create a class that inherits from `unittest.TestCase`. ```python= import unittest from widget import Widget class TestWidget(unittest.TestCase): pass ``` --- ### Writing `unittest` tests Tests are written as methods on the class. - names begin with `test_` - use methods from the `unittest.TestCase` class to make assertions - see assert methods [documentation](https://docs.python.org/3/library/unittest.html#test-cases) ```python= import unittest from widget import Widget class TestWidgetInitialize(unittest.TestCase): def test_initialize_widget_with_color(self): # arrange color = "blue" test_widget = Widget(color) # act result = test_widget.color # assert self.assertEqual(result, color) ``` --- ### The `pytest` package Create a virtual environment if you haven't yet, and install pytest. ```bash pipenv install pytest --python 3.9.4 ``` Run tests at the command line by running ```bash pytest ``` Make a directory called `test` at the top level of your project (be sure it contains a `__init__.py`). Test files must be in `test` directory, and filenames must begin or end with `test`. --- ### Writing `pytest` tests Define test functions directly—no need for classes. Function names must begin with `test` to be treated as a unit test. Use `assert` keyword, followed by the conditional you are trying to test. You can run `unittest` tests with `pytest`, but not vice versa. ```python= from widget import Widget def test_initialize_widget_with_color(): # arrange color = "blue" test_widget = Widget(color) # act result = test_widget.color # assert assert result == color ```