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
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
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
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
clickapp
, and every other argument and command works perfectly.Even this displays the help of my clickapp correctly.
[user@host]$ ./clickapp --help
@click.version_option()
I get the error: --version not implemented
, which is just as expectedWhat 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:
try:
import pkg_resources
except ImportError:
pass
else:
for dist in pkg_resources.working_set:
scripts = dist.get_entry_map().get("console_scripts") or {}
for _, entry_point in iteritems(scripts):
if entry_point.module_name == module:
ver = dist.version
break
-- 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 of install_requires
in your setup.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 as importlib_metadata
) instead of pkg_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 where importlib.metadata
is not in the standard library (< 3.8), the version string lookup will fail.