Namah Shrestha
Posted on October 21, 2022
- This chapter is part of the series:
- Please consider reading the previous chapter (Chapter 1) before moving forward. This chapter builds upon the project structure discussed in the previous chapter (Chapter 1).
-
From the previous chapter (Chapter 1), we have the following project structure:
- README.md - LICENSE - .gitignore - app.py - test_app.py
We would want separate
src
andtests
directories.-
In
test_app.py
we are picking thepytest
solution mentioned in the previous chapter (Chapter1). Because it is a easier to use compared to other testing libraries or frameworks:
import pytest from app import simple_calculator_function def test_simple_calculator_function() -> None: assert simple_calculator_function("5*(4+5)") == 45 # test addition and multiplication assert simple_calculator_function("10 - (100/2)") == -40 # test subtraction and division. assert simple_calculator_function("'a' + 'b'") == 'ab' # test string concatenation
-
Table of Contents:
2.1 Understanding the problem with import statements.
-
Our test file looks something like this:
import pytest from app import simple_calculator_function def test_simple_calculator_function() -> None: assert simple_calculator_function("5*(4+5)") == 45 # test addition and multiplication assert simple_calculator_function("10 - (100/2)") == -40 # test subtraction and division. assert simple_calculator_function("'a' + 'b'") == 'ab' # test string concatenation
The import statement
from app import simple_calculator_function
assumes thatapp.py
is on the same folder as thetest_app.py
and therefore this kind of import works.We do not want to depend on this feature for tests. We want our tests to run no matter where they are.
Here, to make sure that the import works, we would need to make sure that
test_app.py
andapp.py
are in the same directory.-
In case they are in different directories, we need to make sure we run both tests and the application from a common working directory. This working directory is outside both
tests
andsrc
directories
In this case, the import statement in thetest_app.py
file would be:
from <common_working_directory>.<app_directory>.app import simple_calculator_function.
In this case, the tests also need to be run from the
common_working_directory
. The test directory would be:<common_working_directory>/tests/test_*.py
. This stops us from running the tests from anywhere and now we have to depend on having the same working directory as well.
We need to make sure that import works from anywhere and doesn't depend on the directory structure.
The way to make that happen is to make your project an
installable
project.
2.2 Solution to the problem with import statements.
- The solution to the import problem is to turn your project into an
installable
package. - We need to setup before applying the solution. This is what we will do in this section.
-
So lets move on with a new folder structure. We create
src/simple_calculator/
andtests/
directories.
- src/ - simple_calculator/ - __init__.py - app.py - tests/ - __init__.py - test_app.py - .gitignore - LICENSE - README.md
The
app.py
is placed insidesrc/simple_calculator/
along with an__init__.py
file andtest_app.py
is placed insidetests/
along with an__init__.py
file. -
As soon as we create the new directory we get an import error on the test file when we run it.
Unresolved Reference 'app'
- This is because the test file can no longer find
app
. They are in different directories.
-
To make it find the
app
, we can import from the project directory's root and run tests also from the same directory like we mentioned eariler. This would mean the import statement insidetest_app.py
would look like:
from src.simple_calculator.app import simple_calculator_function
Like we mentioned earlier, the problem with this is that we need to run the test also from the project directory.
Again the idea is that would like to run our tests from anywhere without worrying about directory structures for imports.
To enable that we can expect our code to function as a library so that imports can happen from anywhere.
-
This means making our application
installable
. So that the following import works after installing ourpackage
in thevirtual environment
withpip
.
from simple_calculator.app import simple_calculator_function
Now no matter where the test is it can use the same import statement everywhere. This solves the problem of having to depend on directory structure for imports.
2.3 Making our application installable.
- To make our application installable, we need to add a bunch of configuration files.
- This is an open issue in the Python community.
- The idea is, we shouldn’t need these many files to make our application installable.
- Things could have been easier maybe, but, that is how it is at the moment.
- NOTE: The structure that we will follow using
setup.py
,setup.cfg
andpyproject.toml
are not required to be the same always.setup.py
perfectly supports entire functionalities. We can write the entire configurations on any one of these files if we have to. We are just organising our code better. - If you look at the documentation of
setuptools
: https://setuptools.pypa.io/en/latest/index.html. Then you’ll see that each and every configuration field in the example files has a counter part in each of the file types. - The community is pushing the configuration more towards the
toml
file and just leaving the metadata in thecfg
file.
2.3.1 Understanding the pyproject.toml
file.
- The first file we will look at is
pyproject.toml
. - In the early days of Python, there was only one way to install packages. We needed a
setup.py
file. - But nowadays, there are several options such as
Poetry
and other such examples. - We can still stick to using the
setup.py
way and that is what we will be doing in this article. -
We can do this by inserting a
build-backend
to our[build-system]
in ourpyproject.toml
file.
[build-system] requires = ["setuptools>=42.8", "wheel"] build-backend = "setuptools.build_meta"
We have setup
build-backed
to usesetuptools.build_meta
, this will make our project run code insetup.py
. Thebuild-backend
requiressetuptools
and we mention that in therequires
section.So the next file to look at is
setup.py
, because ourbuild-backend
issetuptools
.
2.3.2 Understanding the setup.py
file.
- Next is the
setup.py
file. - In the early days,
setup.py
used to be the place containing the installation script. - It would do everything required to do in order to install a python package.
- We can run arbitrary code inside
setup.py
. Since it is a python script. - This is seen as a security risk and therefore, more and more code is being stripped out of the
setup.py
file and put into one of these other configuration file. -
Let’s create a basic
setup.py
file.
import setuptools if __name__ == "__main__": setuptools.setup()
This basic file is going to allow us to install our package in editable mode. Since, we have mentioned
setuptools.build_meta
inbuild-system
inpyproject.toml
, when we run the install script,setup.py
will be executed.
2.3.3 Understanding the setup.cfg
file.
- To store the metadata of the project such as
Title
andDescription
, we create asetup.cfg
file. -
The file could look as follows:
[metadata] name = something description = just some dummy codebase author = Coding with Zim license = MIT license_file = LICENSE platforms = unix, linux, osx, cygwin, win32 classifiers = Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [options] packages = something install_requires = requests>=2 python_requires = >=3.6 package_dir = =src zip_safe = no
NOTE: In
[options]
we have new line after=
sign. This signifies that there can be more than one of these values. So,packages
,install_requires
,package_dir
can have multiple values.packages
are the names of packages that we are creating.install_requires
are the names of requirements. Need to look at how to do this withrequirements.txt
.python_requires
denotes the versions of python supported.package_dir
is the directory where our application module lives. Inside thesrc
directory. There might be other names. We are just going with the naming convention.zip_safe
isno
. No idea what it is for now.Why use this file instead of putting everything in setup.py?
Since this is just a configuration file and not a python script, we don’t have to worry about it executing arbitrary code which was the case withsetup.py
. This is what the community wants. They want to push everything into different configuration files.-
Then we add a
requirements.txt
file which contains all our dependencies.
requests==2.26.0
In our case, it only contains
requests
for demo purposes.
NOTE: Insetup.py
we gave a version>=2
. Inrequirements.txt
, we specify the actual version. That is best practice. -
With this our project structure looks something like this:
- src/ - something/ - __init__.py - app.py - tests/ - __init__.py - test_app.py - .gitignore - LICENSE - pyproject.toml - README.md - requirements.txt - setup.cfg - setup.py
-
Now we should be able to install it with
pip
by the following command:
your_project_folder$ pip install -e . # e means editable i guess.
Posted on October 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.