Building a Python Package: Developing Locally (4)

by Jarrett Retz February 4th, 2021
python programming building a python package

Introduction

For the fourth installment in the series, I wanted to work on developing the module locally. The library now has a few modules and is capable of sending HTTP requests.

Therefore, I wanted to use the package 'from outside', as if I was a user that just downloaded it. This could be in a Python shell or script.

Also, I thought:

I could also go through how to release a development version (or beta) version. Or maybe the development version could be used to test locally as well?

This turned out to be more work than I expected. Additionally, I don't have the workflow quite right. Regardless, I was able to get the library loaded into a different virtual environment to test it.

Documentation

I started with the guide on distributing packages. Two headers caught my eye:

  1. Working in development
  2. Pre-release versioning

The documentation reads:

Although not required, it’s common to locally install your project in “editable” or “develop” mode while you’re working on it. This allows your project to be both installed and editable in project form. Python Docs

The docs read to execute the command:

pip3 install -e .

In the project root. The . at the end is the path to the current project. Since, theoretically, we navigated into the root project folder, using . means run this command in the current folder.

My First Try

I tried to run this command but got the following error.

ERROR: No .egg-info directory found in /private/var/folders/qw/yqqx13kn74v_mjyknjykpm2h0000gn/T/pip-pip-egg-info-u_t0fji6

I had a suspicion that it wasn't going to work because I had not gone through all the steps for setting up the necessary files for a package (i.e., setup.py, README.md, LICENSE).

This meant that there were a few things I should take care of:

  • setup.py
  • LICENSE
  • README.md
  • Github repository

I'll go through each of these—briefly—and what each does.

setup.py

The setup file is important and can be complicated. Thankfully, there is a sample project that has an example setup.py file with plenty of comments. Also, it specifies what arguments are required and which are optional.

Below is my setup.py file after copy-and-pasting the sample file and making some edits.

# Always prefer setuptools over distutils
from setuptools import setup, find_packages
import pathlib

here = pathlib.Path(__file__).parent.resolve()

# Get the long description from the README file
long_description = (here / 'README.md').read_text(encoding='utf-8')

# Arguments marked as "Required" below must be included for upload to PyPI.
# Fields marked as "Optional" may be commented out.

setup(
    # This is the name of your project. The first time you publish this
    # package, this name will be registered for you. It will determine how
    # users can install this project, e.g.:
    #
    # $ pip install sampleproject
    #
    # And where it will live on PyPI: https://pypi.org/project/sampleproject/
    #
    # There are some restrictions on what makes a valid project name
    # specification here:
    # https://packaging.python.org/specifications/core-metadata/#name
    name='beasy',  # Required

    # Versions should comply with PEP 440:
    # https://www.python.org/dev/peps/pep-0440/
    #
    # For a discussion on single-sourcing the version across setup.py and the
    # project code, see
    # https://packaging.python.org/en/latest/single_source_version.html
    version='1.0.0.dev1',  # Required
    
    license='MIT',

    # This is a one-line description or tagline of what your project does. This
    # corresponds to the "Summary" metadata field:
    # https://packaging.python.org/specifications/core-metadata/#summary
    description='Simple Python API client for accessing data on the Bureau of Economic Analysis application programming interface.',  # Optional

    # This is an optional longer description of your project that represents
    # the body of text which users will see when they visit PyPI.
    #
    # Often, this is the same as your README, so you can just read it in from
    # that file directly (as we have already done above)
    #
    # This field corresponds to the "Description" metadata field:
    # https://packaging.python.org/specifications/core-metadata/#description-optional
    long_description=long_description,  # Optional

    # Denotes that our long_description is in Markdown; valid values are
    # text/plain, text/x-rst, and text/markdown
    #
    # Optional if long_description is written in reStructuredText (rst) but
    # required for plain-text or Markdown; if unspecified, "applications should
    # attempt to render [the long_description] as text/x-rst; charset=UTF-8 and
    # fall back to text/plain if it is not valid rst" (see link below)
    #
    # This field corresponds to the "Description-Content-Type" metadata field:
    # https://packaging.python.org/specifications/core-metadata/#description-content-type-optional
    long_description_content_type='text/markdown',  # Optional (see note above)

    # This should be a valid link to your project's main homepage.
    #
    # This field corresponds to the "Home-Page" metadata field:
    # https://packaging.python.org/specifications/core-metadata/#home-page-optional
    url='https://github.com/jdretz/simple-bea-client',  # Optional

    # This should be your name or the name of the organization which owns the
    # project.
    author='Jarrett Retz',  # Optional

    # This should be a valid email address corresponding to the author listed
    # above.
    author_email='jretz@jrts.info',  # Optional

    # Classifiers help users find your project by categorizing it.
    #
    # For a list of valid classifiers, see https://pypi.org/classifiers/
    classifiers=[  # Optional
        # How mature is this project? Common values are
        #   3 - Alpha
        #   4 - Beta
        #   5 - Production/Stable
        'Development Status :: 3 - Alpha',

        # Indicate who your project is intended for
        'Intended Audience :: Developers',
        'Topic :: Software Development :: Build Tools',

        # Pick your license as you wish
        'License :: OSI Approved :: MIT License',

        # Specify the Python versions you support here. In particular, ensure
        # that you indicate you support Python 3. These classifiers are *not*
        # checked by 'pip install'. See instead 'python_requires' below.
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3.9',
        'Programming Language :: Python :: 3 :: Only',
    ],

    # This field adds keywords for your project which will appear on the
    # project page. What does your project relate to?
    #
    # Note that this is a list of additional keywords, separated
    # by commas, to be used to assist searching for the distribution in a
    # larger catalog.
    keywords='bea, client, analysis',  # Optional

    # When your source code is in a subdirectory under the project root, e.g.
    # `src/`, it is necessary to specify the `package_dir` argument.
    # package_dir={'': 'bea'},  # Optional

    # You can just specify package directories manually here if your project is
    # simple. Or you can use find_packages().
    #
    # Alternatively, if you just want to distribute a single Python file, use
    # the `py_modules` argument instead as follows, which will expect a file
    # called `my_module.py` to exist:
    #
    #   py_modules=["my_module"],
    #
    packages=find_packages(where='beasy'),  # Required

    # Specify which Python versions you support. In contrast to the
    # 'Programming Language' classifiers above, 'pip install' will check this
    # and refuse to install the project if the version does not match. See
    # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
    python_requires='>=3.6, <4',

    # This field lists other packages that your project depends on to run.
    # Any package you put here will be installed by pip when your project is
    # installed, so they must be valid existing projects.
    #
    # For an analysis of "install_requires" vs pip's requirements files see:
    # https://packaging.python.org/en/latest/requirements.html
    install_requires=['requests'],  # Optional

    # List additional groups of dependencies here (e.g. development
    # dependencies). Users will be able to install these using the "extras"
    # syntax, for example:
    #
    #   $ pip install sampleproject[dev]
    #
    # Similar to `install_requires` above, these must be valid existing
    # projects.
    # extras_require={  # Optional
    #     'dev': ['check-manifest'],
    #     'test': ['coverage'],
    # },

    # If there are data files included in your packages that need to be
    # installed, specify them here.
    # package_data={  # Optional
    #     'sample': ['package_data.dat'],
    # },

    # Although 'package_data' is the preferred approach, in some case you may
    # need to place data files outside of your packages. See:
    # http://docs.python.org/distutils/setupscript.html#installing-additional-files
    #
    # In this case, 'data_file' will be installed into '<sys.prefix>/my_data'
    # data_files=[('my_data', ['data/data_file'])],  # Optional

    # To provide executable scripts, use entry points in preference to the
    # "scripts" keyword. Entry points provide cross-platform support and allow
    # `pip` to create the appropriate form of executable for the target
    # platform.
    #
    # For example, the following would provide a command called `sample` which
    # executes the function `main` from this package when invoked:
    # entry_points={  # Optional
    #     'console_scripts': [
    #         'beasy=beasy.beasy:Bea',
    #     ],
    # },

    # List additional URLs that are relevant to your project as a dict.
    #
    # This field corresponds to the "Project-URL" metadata fields:
    # https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use
    #
    # Examples listed include a pattern for specifying where the package tracks
    # issues, where the source is hosted, where to say thanks to the package
    # maintainers, and where to support the project financially. The key is
    # what's used to render the link text on PyPI.
    # project_urls={  # Optional
    #     'Bug Reports': 'https://github.com/pypa/sampleproject/issues',
    #     'Funding': 'https://donate.pypi.org',
    #     'Say Thanks!': 'http://saythanks.io/to/example',
    #     'Source': 'https://github.com/pypa/sampleproject/',
    # },
)

This file created a list of other things I needed to do.

Naming

First, I had to choose a name. Following the PEP8 guide for naming modules, I chose the name beasy for "easy BEA client".

That was sufficiently clever and cute enough for a Python module. I renamed my main module folder and child file to reflect the newly chosen name.

.
├── LICENSE
├── Pipfile
├── Pipfile.lock
├── README.md
├── app.py
├── beasy
│   ├── __init__.py
│   ├── base.py
│   ├── beasy.py
│   └── tables
       │   └── tables.cpython-39.pyc
│       └── tables.py
├── setup.py
└── tests

Versioning

Next, I chose a version. Python lays out guidelines for versioning in PEP440. I looked up the difference between alpha and beta versions, but there wasn't a clear consensus. To the best of my understanding, alpha versions have features available, with more being added.

I don't even think we can call this an alpha version, so let's start with a dev release dev1.

After writing this article, I found this in the documentation

While they may be useful for continuous integration purposes, publishing developmental releases of pre-releases to general-purpose public index servers is strongly discouraged, as it makes the version identifier difficult to parse for human readers. If such a release needs to be published, it is substantially clearer to instead create a new pre-release by incrementing the numeric component.

I will probably release it as an alpha or beta version when I actually publish it as a pre-release. For now, locally, we can keep it as a development release.

License

There's a lot of important legal stuff that goes into licensing that I'm not going to talk about here. If you're serious about it (many people need to be), you should consult a lawyer.

However, this project will be released under an open-source license. Specifically, the MIT license.

I listed this designation in the setup.py file. Then, I went into the LICENSE file and pasted in the below code.

Copyright 2021 Jarrett Retz

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

It's a short license and will save me from any legal action if someone were to use my library for nefarious reasons or if it caused them some unintended damage (it's supposed to protect me).

You can't change a license after the software is released.

Repository

Next, I created an empty repository on my Github account to put the URL in the setup.py file. I did not push any code to the repository yet.

README.md

Finally, I created a short README file. The README file can contain all kinds of good information, like:

  • What the project is about
  • Contributing
  • Examples
  • How-Tos
  • Use cases

A popular option for READMEs is writing them in markdown (.md).

# Simple BEA Client

------------

## Installation

`pip install beasy`

## Description

This library is an unofficial Python API client for [U.S Bureau of Economic Analysis](https://www.bea.gov/ "U.S Bureau of Economic Analysis") API.

You can view the developer guide for the API [here](https://apps.bea.gov/api/_pdf/bea_web_service_api_user_guide.pdf "here"). This is **not** the user guide for this client library. However, it will provide useful information on how this library interacts with the API.

## Purpose

Other BEA client libraries exist for accessing the API. However, in my opinion, they are either too complicated or poorly conceived. Therefore, this library is a simple and straightforward client.

If complicated sorting, filtering, or formatting functions are desired, they will be added as utility functions that are not part of the client module.

## Examples

Coming soon

After doing all of this, it was time to try again.

Developing Locally

I'm going to omit the part where I tried and failed the second time. I think I was entering arguments into setup.py that were unnecessary. I found a suggestion online to create a virtual environment from outside the project and download it into the environment.

I opened up a new terminal and navigated out of my project's root directory. I was in my home folder now.

To create a new virtual environment, I ran:

python3 -m venv env

Then, I activated the environment with:

source env/bin/activate

Next, I installed the module with the e-flag:

pip install -e Programming/bea

The last argument was the path to the root folder. This will be different on someone else's computer.

After running the install function, I noticed a new folder appeared in my project's root directory.

├── beasy.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── requires.txt
│   └── top_level.txt

Additionally, I got the output:

0btaining file:///Users/jarrettretz/Programming/bea
Requirement already satisfied: requests in /opt/anaconda3/lib/python3.8/site-packages (from beasy==1.0.0.dev1) (2.25.0)
Requirement already satisfied: certifi>=2017.4.17 in /opt/anaconda3/lib/python3.8/site-packages (from requests->beasy==1.0.0.dev1) (2020.11.8)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in /opt/anaconda3/lib/python3.8/site-packages (from requests->beasy==1.0.0.dev1) (1.25.11)
Requirement already satisfied: idna<3,>=2.5 in /opt/anaconda3/lib/python3.8/site-packages (from requests->beasy==1.0.0.dev1) (2.10)
Requirement already satisfied: chardet<4,>=3.0.2 in /opt/anaconda3/lib/python3.8/site-packages (from requests->beasy==1.0.0.dev1) (3.0.4)
Installing collected packages: beasy
  Running setup.py develop for beasy
Successfully installed beasy

This all seemed like a good sign.

I started a Python shell in my terminal by running the command python.

After the arrow prompts appeared, I executed two statements:

>>> from beasy.beasy import Bea

>>> client = Bea()

That threw the error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 1 required positional argument: 'api_key'

Perfect! That's the error that I should receive!

Next Article

In the next article, I'm going to due the pre-release. Additionally, I will use Python's 'test' index, which allows people to do a dry-run of uploading their package to the real package index.