---
# System prepended metadata

title: (Archive) Python dependency management and unit-testing

---

<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
```