__version__ traps and best practices

When distributing a Python module, you need to describe its version number,

Two descriptions are required. Of course, writing the same version number in two places is troublesome and a source of mistakes, so I want to do it in one place.

Write the version number in __version__.py and

mylibrary/__version__.py


__version_info__ = (1, 0, 0)
__version__ = '.'.join(map(str, __version_info__))

I think the configuration of ʻimport __version __ from setup.pyand__init __.py` is a common pattern.

\ _ \ _ Version \ _ \ _ trap

There is a problem with this configuration. In __init__.py (or the core module imported by __init__.py), This is the case when import of non-standard module is described.

mylibrary/__init__.py


from __version__ import __version__
from core import MyClass  # mylibrary.Shortcuts for use in MyClass

mylibrary/core.py


import numpy as np  #Import of external modules required by MyClass

class MyClass(object):
    pas

If you execute setup.py in this state, ...

Sudden Import Error


> py setup.py install
Traceback (most recent call last):
  File "C:\Users\hoge\git\example\setup.py", line 4, in <module>
    from mylibrary import __version__
  File "C:\Users\hoge\git\example\mylibrary\__init__.py", line 2, in <module>
    from .core import MyClass
  File "C:\Users\hoge\git\example\mylibrary\core.py", line 1, in <module>
    import numpy as np
ImportError: No module named numpy

An ImportError will occur and the mylibrary module cannot be installed.

Even though install_requires for installing numpy is in setup.py It is a "key in the safe" state that the setup.py cannot be executed and cannot be installed.

Best practice [^ 1]

[^ 1]: I was really planning to write this year's Advent Calendar as a "best practice", but just before I found an overwhelmingly easy way, it was totally crap. I'm sorry, so I'll leave it as it is. Please refer to those who are using a transcendental old environment where setup.cfg cannot be used.

The cause is that if you import __version__.py, __init__.py in the same directory will also be imported. So the solution is to "load __version__.py without importing ".

setup.py


#!/usr/bin/python

from setuptools import setup, find_packages
# from __version__ import __version__  #Delete, cause of ImportError
import os

packages = find_packages()

ns = dict()
for package in packages:
    version_file = os.path.join(package, '__version__.py')
    if os.path.exists(version_file):
        with open(version_file, mode='rt') as f:
            eval(compile(f.read(), version_file, 'exec'), dict(), ns)
            break

__version__ = ns['__version__']
del ns

setup(
    version=__version__,
)

I won't explain it in detail, but I'm looking for __version__.py, evaluating it, and extracting the variable __version__ directly. This way you can avoid importing __init__.py which causes ʻImportError`.

but, I'm reluctant to write too much code other than metadata in setup.py It is not realistic to import a non-standard module with __init__.py.

It seems that something like "an error occurred when trying to combine the version number description in one place" can easily occur. It's no wonder that there is an opinion that "\ _ \ _ version \ _ \ _ is an anti-pattern".

best practice

Write setup.py as follows.

setup.py


from setuptools import setup
setup()

As you can see, all you have to do is call setup () and the settings are empty.

setup () retrieves the missing settings from the setup.cfg file, if any, regardless of whether it is empty or not. And, setup.cfg has a description method to refer to by specifying files and variables.

setup.cfg


[metadata]
version = attr: mylibrary.__version__.__version__

This has the same effect as applying the variable __version__ in the file mylibrary / __ version__.py to the version insetup ().

By the way, with this method as well as importing __version__.py,

However, the reference of the variable described in setup.cfg is the same as setup.py in "Not the best practice", It seems to ʻeval ()` the file directly without going through the import mechanism, and it does not cause an ImportError.

Summary

In addition to version, most things that were described in setup.py can be described smartly in setup.cfg. Also, not only the metadata of setup () but also the settings such as flake8, py.test, and nosetest can be described together. It also helps keep the directory clean.

Since I put setup.cfg with much effort, I want to make the most of it.

Recommended Posts

__version__ traps and best practices
Best practices for Django views.py and urls.py (?)
DBSCAN practices and algorithms
Learn best practices from cookiecutter-django
Build and run TOPPERS / ASP (2020-03-10 version)
Anaconda and Python version correspondence table
AWS Lambda Development My Best Practices
Configuration file best practices in Flask