Search code examples
pythonentry-point

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.


Solution

  • 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