How to write entry_points argument for multiple Click CLI functions? - python

I'm trying to configure setuptools and Click module for multiple functions.
Click documentation instructs in Nesting Commands section to use click.group().
How do you write the entry_points for multiple CLick CLI functions?
I was toying with they syntax, and I managed to get something working but I can't recreate it. I was something like this,
entry_points='''
[console_scripts]
somefunc=yourscript:somefunc
morefunc=yourscript:morefunc
'''
Following the sample given below, I converted the syntax to a dictionary:
entry_points= {'console_scripts':
['somefunc = yourscript:somefunc',
'morefunc = yourscript:morefunc'
]},
After I reinstalled, calling the script raised this error:
(clickenv) > somefunc
Traceback (most recent call last):
[...]
raise TypeError('Attempted to convert a callback into a '
TypeError: Attempted to convert a callback into a command twice.
The way I made this work the first time, was I installed the script, and then gradually changed the code through the various examples. At one point, just as described in the docs, I called the script with $ yourscript somefunc. However, when I recreated the pattern in my project I got that error.
Here I've uninstalled and reinstalled (even though its advertised as unnecessary, pip install -e .) and removed the second entrypoint. Here's my testing example. The function morefunc requires a .txt input file.
# yourscript.py
import click
#click.command()
#click.group()
def cli():
pass
#cli.command()
def somefunc():
click.echo('Hello World!')
#cli.command()
#click.argument('input', type=click.File('rb'))
#click.option('--saveas', default='HelloWorld.txt', type=click.File('wb'))
def morefunc(input, saveas):
while True:
chunk = input.read(1024)
if not chunk:
break
saveas.write(chunk)
# setup.py
from setuptools import setup
setup(
name='ClickCLITest',
version='0.1',
py_modules=['yourscript'],
install_requires=[
'Click',
],
entry_points= {'console_scripts':
['somefunc = yourscript:somefunc']},
)

https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation
setup(
…
entry_points={
'console_scripts': [
'somefunc=yourscript:somefunc',
'morefunc=yourscript:morefunc',
],
},
…
)

Related

Differentiating between an imported package and one run at the CLI when using Entrypoints in Python

I have a python package that most commonly used as a CLI tool, but I sometimes run it as a library for my own purposes. (e.g. turning it into a web-app or for unit testing)
For example, I'd like to sys.exit(1) on error to give the end-user a nice error message on exception when used as a CLI command, but raise an Exception when it's used as an imported library.
I am using an entry_points in my setup.py:
entry_points={
'console_scripts': [
'jello=jello.cli:main'
]
}
This works great, but I can't easily differentiate when the package is run at the CLI or it was imported because __name__ is always jello.cli. This is because the Entrypoint is basically importing the package as normal.
I tried creating a __main__.py file and pointing my Entrypoint there, but that did not seem to make a difference.
I'm considering checking sys.argv[0] to see if my program name exists there, but that seems to be a brittle hack. (in case the user aliases the command or something) Any other ideas or am I just doing it wrong? For now I have been passing an as_lib argument to my functions so they will behave differently based on whether they are loaded as a module or run from the CLI, but I'd like to get away from that.
This is a minimal example of a package structure that can be used as a cli and a library simultaneously.
This is the directory structure:
egpkg/
├── setup.py
└── egpkg/
├── __init__.py
├── lib.py
└── cli.py
This is the entry_points in setup.py. It's identical to yours:
entry_points={
"console_scripts": [
"egpkg_cli=egpkg.cli:main",
],
},
__init__.py:
from .lib import func
cli.py
This is where you will define your CLI and handle any issues that your functions, that you define in other python files, raise.
import sys
import argparse
from egpkg import func
def main():
p = argparse.ArgumentParser()
p.add_argument("a", type=int)
args = vars(p.parse_args())
try:
result = func(**args)
except Exception as e:
sys.exit(str(e))
print(f"Got result: {result}", file=sys.stdout)
lib.py
This is where the core of your library is defined, and you should use the library how you would expect your users to use it. When you get values/input that won't work you can raise it.
def func(a):
if a == 0:
raise ValueError("Supplied value can't be 0")
return 10 / a
Then from in the python console or a script you can:
In [1]: from egpkg import func
In [2]: func(2)
Out[2]: 5.0
In [3]: func(0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/tmp/egpkg/egpkg/lib.py", line 3, in func
raise ValueError("Supplied value can't be 0")
ValueError: Supplied value can't be 0
Supplied value can't be 0
And from the CLI:
(venv) ~ egpkg_cli 2
Got result: 5.0
(venv) ~ egpkg_cli 0
Supplied value can't be 0
I believe what you want is called a 'main guard' in Python. The module knows its name at any given time. If the module is executed from the CLI, its name is __main__. So, what you can do is put all functionality you want in some function at the module level, and then you can call it differently from within the 'main guard' from how you'd call it as a library. Here's what a 'main guard' looks like:
def myfunc(thing, stuff):
if not stuff:
raise MyExc
if __name__ == '__main__':
try:
myfunc(x, y)
except MyExc:
sys.exit(1)
Here, the sys.exit is only called on error if the module is executed from the command line as a script.

How to set entry point for console script with multiple command groups for Python Click?

Given that my library with foobar.py is setup as such:
\foobar.py
\foobar
\__init__.py
\setup.py
Hierarchy of CLI in the console script:
foobar.py
\cli
\foo
\kungfu
\kungpow
\bar
\blacksheep
\haveyouanywool
[code]:
import click
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
#click.group()
#click.version_option()
def cli():
pass
#cli.group(context_settings=CONTEXT_SETTINGS)
def foo():
pass
#cli.group(context_settings=CONTEXT_SETTINGS)
def bar():
pass
#foo.command('kungfu')
def kungfu():
print('bruise lee')
#foo.command('kungpow')
def kungpow():
print('chosen one')
#bar.command('blacksheep')
def blacksheep():
print('bah bah blacksheep')
#bar.command('haveyouanywool')
def haveyouanywool():
print('have you any wool?')
How should I set my entry in setup.py?
There are many examples but they only show a single command for a single entry point, e.g. Entry Points in setup.py
But is it even possible to setup the console script with how the my foobar.py click script is structured?
If not, how should I restructure the commands in foobar.py?
For context, I have this script for the sacremoses library: https://github.com/alvations/sacremoses/blob/cli/sacremoses.py
But I couldn't figure how to configure the setup.py to install the sacremoses.py script properly: https://github.com/alvations/sacremoses/blob/cli/setup.py
To make the entry points work in your example you need:
entry_points='''
[console_scripts]
command_line_name=foobar:cli
''',
What you are missing is an understanding of the meaning of:
command_line_name=foobar:cli
[console_scripts]
There are three things in command_line_name=foobar:cli:
Name of the script from the command line (command_line_name)
Module where the click command handler is located (foobar)
Name of the click command/group in that module (cli)
setup.py
For your github example, I would suggest:
from distutils.core import setup
import setuptools
console_scripts = """
[console_scripts]
sacremoses=sacremoses.cli:cli
"""
setup(
name='sacremoses',
packages=['sacremoses'],
version='0.0.7',
description='SacreMoses',
long_description='LGPL MosesTokenizer in Python',
author='',
license='',
package_data={'sacremoses': [
'data/perluniprops/*.txt',
'data/nonbreaking_prefixes/nonbreaking_prefix.*'
]},
url='https://github.com/alvations/sacremoses',
keywords=[],
classifiers=[],
install_requires=['six', 'click', 'joblib', 'tqdm'],
entry_points=console_scripts,
)
Command Handler
In the referenced branch of your github repo, there is NO cli.py file. The [code] from your question needs to be saved in sacremoses/cli.py, and then combined with the suggested changes to your setup.py, everything should work fine.

How to add a custom nose plugin to the `nosetests` command

So I'm very noob in dealing with nose plugins.
I've been searching a lot but docs regarding nose plugins seem scarce.
I read and tried what's in the following links to try to write a simple nose plugin
and run it with nosetests, without success:
https://nose.readthedocs.org/en/latest/doc_tests/test_init_plugin/init_plugin.html
https://nose.readthedocs.org/en/latest/plugins/writing.html
I don't want to write my own test-runner or run the tests from any other script (via run(argv=argv, suite=suite(), ...)),
like they do in the first link.
I wrote a file myplugin.py with a class like this:
import os
from nose.plugins import Plugin
class MyCustomPlugin(Plugin):
name = 'myplugin'
def options(self, parser, env=os.environ):
parser.add_option('--custom-path', action='store',
dest='custom_path', default=None,
help='Specify path to widget config file')
def configure(self, options, conf):
if options.custom_path:
self.make_some_configs(options.custom_path)
self.enabled = True
def make_some_configs(self, path):
# do some stuff based on the given path
def begin(self):
print 'Maybe print some useful stuff...'
# do some more stuff
and added a setup.py like this:
try:
from setuptools import setup, find_packages
except ImportError:
import distribute_setup
distribute_setup.use_setuptools()
from setuptools import setup, find_packages
setup(
name='mypackage',
...
install_requires=['nose==1.3.0'],
py_modules=['myplugin'],
entry_points={
'nose.plugins.1.3.0': [
'myplugin = myplugin:MyCustomPlugin'
]
}
)
Both files are in the same directory.
Every time I run nosetests --custom-path [path], I get:
nosetests: error: no such option: --custom-path
From the links mentioned above, I thought that's all that was required to register and enable a custom plugin.
But it seems that, either I'm doing something really wrong, or nose's docs are outdated.
Can someone please point me the correct way to register and enable a plugin, that I can use with nosetests?
Thanks a lot!! :)
You don't want the nose version in entry_points in setup.py. Just use nose.plugins.0.10 as the docs say. The dotted version in the entry point name is not so much a nose version as a plugin API version.

Distribute, epydoc, and setup.py

I would like to have a target (say docs) that runs epydoc for my package. I assume that I need to create a new command but I am not having much luck.
Has anyone done this before?
The Babel project provides several commands for use in setup.py files.
You need to define a distutils.commands entry point with commands; example from the Babel setup.py file:
entry_points = """
[distutils.commands]
compile_catalog = babel.messages.frontend:compile_catalog
extract_messages = babel.messages.frontend:extract_messages
init_catalog = babel.messages.frontend:init_catalog
update_catalog = babel.messages.frontend:update_catalog
"""
where the extra commands are then available as python setup.py commandname.
The entry points point to subclasses of from distutils.cmd import Command. Example again from Babel, from the babel.messages.frontend module:
from distutils.cmd import Command
from distutils.errors import DistutilsOptionError
class compile_catalog(Command):
"""Catalog compilation command for use in ``setup.py`` scripts."""
# Description shown in setup.py --help-commands
description = 'compile message catalogs to binary MO files'
# Options available for this command, tuples of ('longoption', 'shortoption', 'help')
# If the longoption name ends in a `=` it takes an argument
user_options = [
('domain=', 'D',
"domain of PO file (default 'messages')"),
('directory=', 'd',
'path to base directory containing the catalogs'),
# etc.
]
# Options that don't take arguments, simple true or false options.
# These *must* be included in user_options too, but without a = equals sign
boolean_options = ['use-fuzzy', 'statistics']
def initialize_options(self):
# Set a default for each of your user_options (long option name)
def finalize_options(self):
# verify the arguments and raise DistutilOptionError if needed
def run(self):
# Do your thing here.

Create different distribution types with setup.py

Given the following (demonstration) project layout:
MyProject/
README
LICENSE
setup.py
myproject/
... # packages
extrastuff/
... # some extra data
How (and where) do I declare different distribution types? Especially I need these two options:
A distribution containing only the source
A distribution containing the source and all data files under (extrastuff)
Ideally, how do I declare the upper two configuration whereas the second one depends on the first one?
I've implemented something like this before ... the sdist command can be extended to handle additional command line arguments and to manipulate the data files based on these. If you run python setup.py sdist --help, it'll include your custom command line arguments in the help, which is nice. Use the following recipe:
from distutils import log
from distutils.core import setup
from distutils.command.sdist import sdist
class CustomSdist(sdist):
user_options = [
('packaging=', None, "Some option to indicate what should be packaged")
] + sdist.user_options
def __init__(self, *args, **kwargs):
sdist.__init__(self, *args, **kwargs)
self.packaging = "default value for this option"
def get_file_list(self):
log.info("Chosen packaging option: {self.packaging}".format(self=self))
# Change the data_files list here based on the packaging option
self.distribution.data_files = list(
('folder', ['file1', 'file2'])
)
sdist.get_file_list(self)
if __name__ == "__main__":
setup(
name = "name",
version = "version",
author = "author",
author_email = "author_email",
url = "url",
py_modules = [
# ...
],
packages = [
# ...
],
# data_files = default data files for commands other than sdist if you wish
cmdclass={
'sdist': CustomSdist
}
)
You could extend setup.py to additionally include some custom command line parsing. You could then catch a custom argument and strip it out so that it won't affect setuptools.
You can access the command line argument in sys.argv. As for modifying the call to setuptools.setup(), I recommend creating a dictionary of arguments to pass, modifying the dictionary based on the command line arguments, and then calling setup() using the **dict notation, like so:
from setuptools import setup
import sys
basic = {'name': 'my program'}
extra = {'bonus': 'content'}
if '--extras' in sys.argv:
basic.update(extra)
sys.argv.remove('--extras')
setup(**basic)
For more thorough command line parsing you could also use the getopt module, or the newer argparse module if you're only targeting Python 2.7 and higher.
EDIT: I also found a section in the distutils documentation titled Creating a new Distutils command. That may also be a helpful resource.

Categories