Python Click CLI Application
When you use a Click library to build a Python CLI Application you can do this:
@click.version_option()
def cli():
'''
Main Entry Point to Click Interface
'''
to be able to do this:
[user@host]$ clickapp --version
Click Packaged in pex
But when I package it as a pex
file, every other argument, option, commands, sub-command of my click application works, except --version
.
When I run (clickapp is now a binary executable pex
file):
[user@host]$ ./clickapp --version
I get the following error:
Traceback (most recent call last):
File "/~/clickapp/.bootstrap/pex/pex.py", line 446, in execute
File "/~/clickapp/.bootstrap/pex/pex.py", line 378, in _wrap_coverage
File "/~/clickapp/.bootstrap/pex/pex.py", line 409, in _wrap_profiling
File "/~/clickapp/.bootstrap/pex/pex.py", line 508, in _execute
File "/~/clickapp/.bootstrap/pex/pex.py", line 610, in execute_entry
File "/~/clickapp/.bootstrap/pex/pex.py", line 626, in execute_pkg_resources
File "/~/.pex/installed_wheels/7bcb1b5bf49ca3f89c348c338d33c04e3d59dfc1/click-7.1.2-py2.py3-none-any.whl/click/core.py", line 829, in __call__
return self.main(*args, **kwargs)
File "/~/.pex/installed_wheels/7bcb1b5bf49ca3f89c348c338d33c04e3d59dfc1/click-7.1.2-py2.py3-none-any.whl/click/core.py", line 781, in main
with self.make_context(prog_name, args, **extra) as ctx:
File "/~/.pex/installed_wheels/7bcb1b5bf49ca3f89c348c338d33c04e3d59dfc1/click-7.1.2-py2.py3-none-any.whl/click/core.py", line 700, in make_context
self.parse_args(ctx, args)
File "/~/.pex/installed_wheels/7bcb1b5bf49ca3f89c348c338d33c04e3d59dfc1/click-7.1.2-py2.py3-none-any.whl/click/core.py", line 1212, in parse_args
rest = Command.parse_args(self, ctx, args)
File "/~/.pex/installed_wheels/7bcb1b5bf49ca3f89c348c338d33c04e3d59dfc1/click-7.1.2-py2.py3-none-any.whl/click/core.py", line 1048, in parse_args
value, args = param.handle_parse_result(ctx, opts, args)
File "/~/.pex/installed_wheels/7bcb1b5bf49ca3f89c348c338d33c04e3d59dfc1/click-7.1.2-py2.py3-none-any.whl/click/core.py", line 1630, in handle_parse_result
value = invoke_param_callback(self.callback, ctx, self, value)
File "/~/.pex/installed_wheels/7bcb1b5bf49ca3f89c348c338d33c04e3d59dfc1/click-7.1.2-py2.py3-none-any.whl/click/core.py", line 123, in invoke_param_callback
return callback(ctx, param, value)
File "/~/.pex/installed_wheels/7bcb1b5bf49ca3f89c348c338d33c04e3d59dfc1/click-7.1.2-py2.py3-none-any.whl/click/decorators.py", line 295, in callback
raise RuntimeError("Could not determine version")
RuntimeError: Could not determine version
Details
The setup.py
file:
from setuptools import setup, find_namespace_packages
setup(
name='clickapp',
version='0.1.3',
author='Hamza M Zubair',
packages=find_namespace_packages(),
include_package_data=True,
package_data={
'': ['*.yaml'],
},
classifiers=[
'Programming Language :: Python :: 3',
'Operating System :: OS Independent',
'Natural Language :: English',
'License :: Other/Proprietary License',
],
python_requires='>=3.6',
install_requires=[
'click',
'pandas',
'sqlalchemy',
'jinjasql',
'pyyaml',
'joblib',
'python-dateutil',
'loguru',
'pymysql',
'xgboost',
'sklearn',
'wheel',
'importlib-resources'
],
entry_points='''
[console_scripts]
clickapp=clickapp.cli:cli
''',
)
The command used to create the pex
file:
[user@host]$ python setup.py bdist_pex --bdist-all
Tool Specs
I am building and running the pex file in different systems, using the following versions of libraries/packages. The target machine only has Python and no libraries, because pex file does not require libraries/virtualenv etc.
Build Machine OS: CentOS Linux release 7.8.2003 (Core)
Build Machine Python: 3.6.8
setuptools: 51.0.0
pex: 2.1.21
click: 7.1.2
Target Machine OS: CentOS Linus release 7.4.1708 (Core)
Target Machine Python: 3.6.8
What I tried
- I have tested the full functionality of my
clickapp
, and every other argument and command works perfectly.
Even this displays the help of my clickapp correctly.
[user@host]$ ./clickapp --help
- I tried re-building the package several times
- I have only tested this in Python3.6. I haven't tried different python versions, its slightly difficult to set it up in both the source and target systems.
- When I removed
@click.version_option()
I get the error:--version not implemented
, which is just as expected - I am yet to test it on a 2nd target system, in case some idiosyncrasies of my current target server is causing the error
More Info
What other information should I provide, for helping SO users?
Short answer:
setuptools
is missing.It looks like you have click v7.1.2. In that version one of the code paths to figure out the version number automatically uses
pkg_resources
which is a top-level package of setuptools:-- https://github.com/pallets/click/blob/7.1.2/src/click/decorators.py#L283-L293
So in a way, click depends on setuptools. But there are other code paths that do not require
pkg_resources
, for example the version number can be set explicitly in the parameters of the decorator (if I am not mistaken):@click.version_option(version='1.2.3')
(doc), in such a case setuptools is not a mandatory dependency.In most cases, it just works because (out of what I would call a coincidence) setuptools is almost always pre-installed (in virtual environments for example). But "almost always" is not the same as "always", and for example it is absolutely possible to have environments (virtual or not) that do not have a setuptools installation. And this looks like it is exactly what happens in the case of a pex-packaged application. I believe pex creates clean virtual environments (i.e. without pip, setuptools or wheel or anything else).
Since setuptools is not declared as a dependency it is not installed, importing
pkg_resources
fails, and finding the version string fails.I would say that this is a failure on click's side. They should either declare setuptools as a mandatory dependency, or at least as an optional dependency (in an extra) and document this properly.
A possible fix is to add setuptools as a direct dependency of your application: add
setuptools
to the list ofinstall_requires
in yoursetup.py
.Note that as far as I can tell, things will change in click v8. Finding out the version string will rely on
importlib.metadata
from the standard library (back-ported to Python < 3.8 asimportlib_metadata
) instead ofpkg_resources
:From the looks of it, it will cause problems again, probably even more. Since click still does not declare
importlib-metadata
as a dependency for older Python versions whereimportlib.metadata
is not in the standard library (< 3.8), the version string lookup will fail.