- 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
srcandtestsdirectories.-
In
test_app.pywe are picking thepytestsolution 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_functionassumes thatapp.pyis on the same folder as thetest_app.pyand 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.pyandapp.pyare 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
testsandsrcdirectories
In this case, the import statement in thetest_app.pyfile 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
installableproject.
2.2 Solution to the problem with import statements.
- The solution to the import problem is to turn your project into an
installablepackage. - 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.mdThe
app.pyis placed insidesrc/simple_calculator/along with an__init__.pyfile andtest_app.pyis placed insidetests/along with an__init__.pyfile. -
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.pywould 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 ourpackagein thevirtual environmentwithpip.
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.cfgandpyproject.tomlare not required to be the same always.setup.pyperfectly 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
tomlfile and just leaving the metadata in thecfgfile.
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.pyfile. - But nowadays, there are several options such as
Poetryand other such examples. - We can still stick to using the
setup.pyway and that is what we will be doing in this article. -
We can do this by inserting a
build-backendto our[build-system]in ourpyproject.tomlfile.
[build-system] requires = ["setuptools>=42.8", "wheel"] build-backend = "setuptools.build_meta" We have setup
build-backedto usesetuptools.build_meta, this will make our project run code insetup.py. Thebuild-backendrequiressetuptoolsand we mention that in therequiressection.So the next file to look at is
setup.py, because ourbuild-backendissetuptools.
2.3.2 Understanding the setup.py file.
- Next is the
setup.pyfile. - In the early days,
setup.pyused 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.pyfile and put into one of these other configuration file. -
Let’s create a basic
setup.pyfile.
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_metainbuild-systeminpyproject.toml, when we run the install script,setup.pywill be executed.
2.3.3 Understanding the setup.cfg file.
- To store the metadata of the project such as
TitleandDescription, we create asetup.cfgfile. -
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_dircan have multiple values.packagesare the names of packages that we are creating.install_requiresare the names of requirements. Need to look at how to do this withrequirements.txt.python_requiresdenotes the versions of python supported.package_diris the directory where our application module lives. Inside thesrcdirectory. There might be other names. We are just going with the naming convention.zip_safeisno. 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.txtfile which contains all our dependencies.
requests==2.26.0In our case, it only contains
requestsfor demo purposes.
NOTE: Insetup.pywe 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
pipby the following command:
your_project_folder$ pip install -e . # e means editable i guess.
Top comments (4)
Please avoid full capital letters in title, it gives the impression of aggressiveness.
Thanks will do
"-e ." means install the current directory in editable mode.
Thank you. I shall update the same.