7
\$\begingroup\$

If I have this simple structure:

$ tree
.
└── project
    ├── __init__.py
    └── my_script.py

where I want to define __version__ in __init__.py (this allows setup.py, in the parent directory, to import project.__version__):

$ cat __init__.py
# -*- coding: utf-8 -*-

__version__ = "0.1"

Now, I also want to use the value stored in __version__ from other scripts (possibly in subdirectories). I can write this in my_script.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from __init__ import __version__

print(__version__)

It works, but looks so ugly. Is this really the way to do that?

EDIT:

The from . import __version__ unfortunately doesn't work:

$ ./my_script.py 
Traceback (most recent call last):
  File "./my_script.py", line 4, in <module>
    from . import __version__
SystemError: Parent module '' not loaded, cannot perform relative import
\$\endgroup\$
3
  • \$\begingroup\$ from . import __version__ \$\endgroup\$ May 30, 2016 at 15:48
  • 2
    \$\begingroup\$ @GarethRees Could you please write that as an answer? \$\endgroup\$ May 30, 2016 at 17:01
  • \$\begingroup\$ But this doesn't work, as my_script.py cannot do relative imports (python3). I add the result in an edit. \$\endgroup\$
    – zezollo
    May 30, 2016 at 17:46

1 Answer 1

5
\$\begingroup\$

A better approach is to install your script using a setuptools entry_point and let the argparse handle the parameter parsing. Your project would look something like:

.
├── myproject
│   ├── __init__.py
│   └── cli.py
└── setup.py

Your setup.py would look like:

import setuptools

import myproject


setuptools.setup(
    name='myproject',
    version=myproject.version,
    description='Does stuff',
    url='https://github.com/me/myproject',
    packages=['myproject'],
    entry_points={
        'console_scripts': ['my-script = myproject.cli:entry_point'],
    },
)

When you install your package using pip, it will install a command line script named my-script somewhere like /usr/local/bin. The command line script will load your package and any requirements, then run the entry_point function in your cli module. So let's look at that module:

import argparse
import logging

from . import version


def entry_point():
    parser = argparse.ArgumentParser(
        description='Something ... something short',
        epilog='''Bacon ipsum dolor amet occaecat labore qui, ad
                  veniam boudin capicola flank. Dolore rump jerky magna
                  sirloin pancetta. Voluptate anim non minim nostrud ham.
                  Excepteur kevin turkey officia kielbasa pastrami ad ham
                  nostrud meatball tongue magna t-bone. Chuck salami mollit,
                  fatback pig velit officia ut. Cupidatat nulla jowl, tail
                  chicken occaecat excepteur pig beef salami non. Kevin
                  cupim bresaola filet mignon enim.''')
    parser.add_argument('--verbose', '-v', action='count',
                        help='increase verbosity.  Specify multiple times '
                             'for increased diagnostic output')
    parser.add_argument('--version', action='version',
                        version='%(prog)s {}'.format(version),
                        help='show the version number and exit')

    args = parser.parse_args()
    # do stuff here

if __name__ == '__main__':
    entry_point()

The argparse module handles the --version and --help arguments for you. Since your code is installed and loaded as a proper package, you can use relative imports. Of course, you are free to use import myproject as well.

I usually do my __init__.py file a little differently. I follow the what the standard sys module does:

version_info = (0, 0, 0)
version = '.'.join(str(c) for c in version_info)

This makes it possible to use version_info to compare against the numeric version like version_info > (1, 0) which has some nice benefits.

Here's an example of the package in action:

$ python3.4 -mvenv env
$ env/bin/python setup.py install
$ env/bin/my-script --help
usage: my-script [-h] [--verbose] [--version]

Something ... something short

optional arguments:
  -h, --help     show this help message and exit
  --verbose, -v  increase verbosity. Specify multiple times for increased
                 diagnostic output
  --version      show the version number and exit

Bacon ipsum dolor amet occaecat labore qui, ad veniam boudin capicola flank.
Dolore rump jerky magna sirloin pancetta. Voluptate anim non minim nostrud
ham. Excepteur kevin turkey officia kielbasa pastrami ad ham nostrud meatball
tongue magna t-bone. Chuck salami mollit, fatback pig velit officia ut.
Cupidatat nulla jowl, tail chicken occaecat excepteur pig beef salami non.
Kevin cupim bresaola filet mignon enim. Kielbasa non occaecat flank t-bone ad,
sunt in tail esse deserunt id duis. Ut boudin turducken, pariatur labore
veniam lorem proident culpa laborum. Hamburger swine sunt aliqua, et bacon
shoulder jerky cillum. Meatloaf sed pork belly, dolore corned beef in
consectetur enim ball tip. Fugiat ribeye eiusmod sirloin ground round boudin
shoulder voluptate, reprehenderit tongue landjaeger hamburger. Fatback nisi
occaecat, bacon brisket consequat filet mignon fugiat laboris turducken eu. Ad
tempor frankfurter reprehenderit est corned beef voluptate.
$
$ env/bin/my-script --version
my-script 0.0.0

Then to run myproject/cli.py as a standalone script:

$ python -m myproject.cli --version
cli.py 0.0.0
\$\endgroup\$
3
  • \$\begingroup\$ Sorry for the delay, I was on something else but back on that problem. Thanks for this thorough explanations, I will use the setup.py way. One problem still remains though: now I have my-script working in the venv, but if I want to run the cli.py executable directly, then the same error is still here SystemError: Parent module '' not loaded, cannot perform relative import. Isn't it possible to keep it working too? I would like to have both the installed my-script and the cli.py (dev version) running. Is this another question? \$\endgroup\$
    – zezollo
    Jun 16, 2016 at 17:20
  • \$\begingroup\$ @zezollo - relative imports and running scripts that are within the package really doesn't work that well. See stackoverflow.com/a/28154841/41747 for more details. You can probably run the module as python -m project.my_script but there really is not a good way to run it as my_script.py when it is buried in a package. \$\endgroup\$
    – D.Shawley
    Jun 17, 2016 at 10:50
  • \$\begingroup\$ After some more struggle, I finally managed to have both the venv-installed version and something like python -m project.my_script work (even out of the venv). I will suggest an edit to your answer to put this all together. \$\endgroup\$
    – zezollo
    Jun 17, 2016 at 14:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.