<style>
.present {
text-align: left;
}
img[alt=knight_moves] {
width: 400px;
}
</style>
# (Archive) 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
```
---
### Quick note about `__init__.py`
This file should go in any directory being imported. It will transform a plain old directory into a Python package.
A "package" is a collection of modules.
Upon import from a package, its`__init__.py` file is implicitly executed, and all objects it defines are bound to the module's namespace ([documentation](https://docs.python.org/3/reference/import.html#regular-packages)).
---
### Why do we need `__init__.py` if we can import without it?
Python 3.3+ creates an *implicit namespace package* if no `__init__.py` file exists for a directory. We want to avoid this most of the time!
We need a `__init__.py` if we want to run the directory as a module, if we want to run `pytest` on it, etc.
This file can be completely empty (and often will be). It can also be the place where we initialize our applications!
---
### 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.
---
### Python imports (takeaways)
1. All import statements must be "absolute" - meaning relative to the top level of your project
2. 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).
---
### 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
```