Wednesday, August 3, 2016

How to Write Your Own Python Packages_part1

Overview

Python is a wonderful programming language and much more. One of its weakest points is packaging. This is a well-known fact in the community. Installing, importing, using and creating packages has improved over the years, but it's still not on par with newer languages like Go and Rust that could learn a lot from the struggles of Python and other more mature languages.

In this tutorial, you'll learn everything you need to know to build and share your own packages. For general background on Python packages.

Packaging a Project

Packaging a project is the process by which you take a hopefully coherent set of Python modules and possibly other files and put them in a structure that can be used easily. There are various things you have to consider, such as dependencies on other packages, internal structure (sub-packages), versioning, target audience, and form of package (source and/or binary).

Example
Let's start with a quick example. The conman package is a package for managing configuration. It supports several file formats as well as distributed configuration using etcd.

A package's contents are typically stored in a single directory (although it is common to split sub-packages in multiple directories) and sometimes, as in this case, in its own git repository.

The root directory contains various configuration files (setup.py is mandatory and the most important one), and the package code itself is usually in a subdirectory whose name is the name of the package and ideally a tests directory. Here is what it looks like for "conman":

  1. > tree 
  2. ├── LICENSE 
  3. ├── MANIFEST.in 
  4. ├── README.md 
  5. ├── conman 
  6. │   ├── __init__.py 
  7. │   ├── __pycache__ 
  8. │   ├── conman_base.py 
  9. │   ├── conman_etcd.py 
  10. │   └── conman_file.py 
  11. ├── requirements.txt 
  12. ├── setup.cfg 
  13. ├── setup.py 
  14. ├── test-requirements.txt 
  15. ├── tests 
  16. │   ├── __pycache__ 
  17. │   ├── conman_etcd_test.py
  18. │   ├── conman_file_test.py
  19. │   └── etcd_test_util.py
  20. └── tox.ini
Let's take a quick peek at the setup.py file. It imports two functions from the setuptools package: setup() and find_packages(). Then it calls the setup()function and uses  find_packages()   for one of the parameters.
  1. from setuptools import setup, find_packages 
  2. setup(name='conman',
  3.       version='0.3',
  4.       url='https://github.com/the-gigi/conman',
  5.       license='MIT',
  6.       author='Gigi Sayfan',
  7.       author_email='the.gigi@gmail.com', 
  8.       description='Manage configuration files', 
  9.       packages=find_packages(exclude=['tests']),
  10.       long_description=open('README.md').read(),
  11.       zip_safe=False,
  12.       setup_requires=['nose>=1.0'],
  13.       test_suite='nose.collector')
This is pretty normal. While the setup.py file is a regular Python file and you can do whatever you want in it, its primary job it to call the setup() function with the appropriate parameters because it will be invoked by various tools in a standard way when installing your package. I'll go over the details in the next section.

The Configuration Files

In addition to setup.py, there are a few other optional configuration files that can show up here and serve various purposes.

Setup.py
The setup() function takes a large number of named arguments to control many aspects of package installation as well as running various commands. Many arguments specify metadata used for searching and filtering when uploading your package to a repository.

name: the name of your package (and how it will be listed on PYPI)
version: this is critical for maintaining proper dependency management
url: the URL of your package, typically GitHub or maybe the readthedocs URL
packages: list of sub-packages that need to be included; find_packages() helps here
setup_requires: here you specify dependencies
test_suite: which tool to run at test time
The long_description is set here to the contents of the README.md file, which is a best practice to have a single source of truth.

Setup.cfg
The setup.py file also serves a command-line interface to run various commands. For example, to run the unit tests, you can type: python setup.py test
  1. running test
  2. running egg_info
  3. writing conman.egg-info/PKG-INFO
  4. writing top-level names to conman.egg-info/top_level.txt
  5. writing dependency_links to conman.egg-info/dependency_links.txt
  6. reading manifest file 'conman.egg-info/SOURCES.txt' 
  7. reading manifest template 'MANIFEST.in'
  8. writing manifest file 'conman.egg-info/SOURCES.txt'
  9. running build_ext
  10. test_add_bad_key (conman_etcd_test.ConManEtcdTest) ... ok
  11. test_add_good_key (conman_etcd_test.ConManEtcdTest) ... ok
  12. test_dictionary_access (conman_etcd_test.ConManEtcdTest) ... ok
  13. test_initialization (conman_etcd_test.ConManEtcdTest) ... ok
  14. test_refresh (conman_etcd_test.ConManEtcdTest) ... ok
  15. test_add_config_file_from_env_var (conman_file_test.ConmanFileTest) ... ok
  16. test_add_config_file_simple_guess_file_type (conman_file_test.ConmanFileTest) ... ok
  17. test_add_config_file_simple_unknown_wrong_file_type (conman_file_test.ConmanFileTest) ... ok
  18. test_add_config_file_simple_with_file_type (conman_file_test.ConmanFileTest) ... ok
  19. test_add_config_file_simple_wrong_file_type (conman_file_test.ConmanFileTest) ... ok
  20. test_add_config_file_with_base_dir (conman_file_test.ConmanFileTest) ... ok
  21. test_dictionary_access (conman_file_test.ConmanFileTest) ... ok
  22. test_guess_file_type (conman_file_test.ConmanFileTest) ... ok
  23. test_init_no_files (conman_file_test.ConmanFileTest) ... ok
  24. test_init_some_bad_files (conman_file_test.ConmanFileTest) ... ok
  25. test_init_some_good_files (conman_file_test.ConmanFileTest) ... ok
  26. ----------------------------------------------------------------------
  27. Ran 16 tests in 0.160s
  28. OK
The setup.cfg is an ini format file that may contain option defaults for commands you pass to setup.py. Here, setup.cfg contains some options for nosetests (our test runner):
  1. [nosetests]
  2. verbose=1
  3. nocapture=1
MANIFEST.in
This file contains files that are not part of the internal package directory, but you still want to include. Those are typically the readme file, the license file and similar. An important file is the requirements.txt . This file is used by pip to install other required packages.
Here is conman's MANIFEST.in file:
  1. include LICENSE
  2. include README.md
  3. include requirements.txt
Dependencies
You can specify dependencies both in the install_requires section of setup.py and in a requirements.txt file. Pip will install automatically dependencies from install_requires, but not from the requirements.txt file. To install those requirements, you'll have to specify it explicitly when running pip: pip install -r requirements.txt.
Theinstall_requires option is designed to specify minimal and more abstract requirements at the major version level. The requirements.txt file is for more concrete requirements often with pinned down minor versions.
Here is the requirements file of conman. You can see that all the versions are pinned, which means it can be negatively impacted if one of these packages upgrades and introduces a change that breaks conman.
  1. PyYAML==3.11
  2. python-etcd==0.4.3
  3. urllib3==1.7
  4. pyOpenSSL==0.15.1
  5. psutil==4.0.0
  6. six==1.7.3
Pinning gives you predictability and peace of mind. This is especially important if many people install your package at different times. Without pinning, each person will get a different mix of dependency versions based on when they installed it. The downside of pinning is that if you don't keep up with your dependencies development, you may get stuck on an old, poorly performing and even vulnerable version of some dependency.
I originally wrote conman in 2014 and didn't pay much attention to it. Now, for this tutorial I upgraded everything and there were some major improvements across the board for almost every dependency.
Written by: Gigi Sayfan
If you found this post interesting, follow and support us.
Suggest for you:

No comments:

Post a Comment