Bram Vanroy
Bram Vanroy

Reputation: 28437

Structure of package that can also be run as command line script

I have written a package with the 'standard' minimal structure. It looks like this:

my_package/
    my_package/
        __init__.py
    setup.py

__init__.py contains a class and as such can simply be imported and used as one would expect.

However, the code really lends itself to be used in a command-line-way, e.g.

python my_package --arg1 "I like bananas."

At first, I just had an if __name__ == '__main__' check in __init__ which would then use argparse. This works but it isn't pretty, because it would mean that you'd call it from the command line like so:

python my_package/__init__.py --arg1 "I like bananas."

From what I read, this is where a __main__.py file comes in which would be executed as the default script inside a folder (similar to a index.html file on a website). The idea I have is to then simply import __init__.py, run argparse and feed the arguments to the class constructor. Like so:

import argparse
from __init__ import MyClass

parser = argparse.ArgumentParser()
parser.add_argument("--arg1", help="Some dummy value")

args = parser.parse_args()
my_class = MyClass(**vars(args))
my_class.do_stuff()

Is this how similar packages ought to be structured, or is there a better way?


The above works but PyCharm tells me that in __main__.py __init__ is an unresolved reference. Same for MyClass on that import line. When I use .__init__ instead (with a dot) the warning goes away but then the code doesn't work anymore, giving me a ImportError: attempted relative import with no known parent package.

Upvotes: 7

Views: 1441

Answers (1)

RunOrVeith
RunOrVeith

Reputation: 4805

I want to suggest a different structure to you:

my_package/
    my_package/
        __init__.py
        cli_scripts.py
    setup.py

Let's assume your __init__.py looks like this (as a side note, I'd recommend moving the classes defined in there to a separate file, and then simply importing that file in the __init__):

class Test(object):

    def __init__(self, a)
        self.a = a

    def __call__(self):
        print(self.a)

Now there is an additional file inside the package that utilizes the stuff you implemented in the package, let's call it cli_scripts.py:

import argparse

from my_package import Test


def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument("a", type=int, help="Just an example argument")
    return parser.parse_args()

def demonstration():
    args = parse_args()
    t = Test(a=args.a)
    t()

My suggestion now is to utilize the console_scripts entry point inside setup.py, which could now look something like this:

from setuptools import setup

setup(
    name='testpy',
    version='0.1',
    description='Just an example',
    author='RunOrVeith',
    author_email='[email protected]',
    packages=["my_package"],
    entry_points={
        "console_scripts": ["mypkg=my_package.cli_scripts:demonstration"]},
    install_requires=[],
)

Now when you run pip install . inside the top-level my_package folder, your package will be installed. The entry_points automatically generate an executable for you, that you can now call with the name you gave it inside setup.py, in this example mypkg. This means you can now run

mypkg 5 and this will call demonstration().

This way:

  • you do not need to handle any __main__ files
  • you can give your script a meaningful name
  • you don't need to care whether your script is executable, or specify to run the script with python
  • you can have as many of these as you want inside the list entry_points
  • it forces you to write functions that you could also call from other modules, instead of the __main__

I think this is pretty clean. You can read more about entry_points in this blog, it has more functionalities! If you want to use code specified in __main__ instead of this cli_scripts approach, you can have a look at this question.

Upvotes: 7

Related Questions