Sibling package imports Sibling package imports python python

Sibling package imports


Tired of sys.path hacks?

There are plenty of sys.path.append -hacks available, but I found an alternative way of solving the problem in hand.

Summary

  • Wrap the code into one folder (e.g. packaged_stuff)
  • Create setup.py script where you use setuptools.setup(). (see minimal setup.py below)
  • Pip install the package in editable state with pip install -e <myproject_folder>
  • Import using from packaged_stuff.modulename import function_name

Setup

The starting point is the file structure you have provided, wrapped in a folder called myproject.

.└── myproject    ├── api    │   ├── api_key.py    │   ├── api.py    │   └── __init__.py    ├── examples    │   ├── example_one.py    │   ├── example_two.py    │   └── __init__.py    ├── LICENCE.md    ├── README.md    └── tests        ├── __init__.py        └── test_one.py

I will call the . the root folder, and in my example case it is located at C:\tmp\test_imports\.

api.py

As a test case, let's use the following ./api/api.py

def function_from_api():    return 'I am the return value from api.api!'

test_one.py

from api.api import function_from_apidef test_function():    print(function_from_api())if __name__ == '__main__':    test_function()

Try to run test_one:

PS C:\tmp\test_imports> python .\myproject\tests\test_one.pyTraceback (most recent call last):  File ".\myproject\tests\test_one.py", line 1, in <module>    from api.api import function_from_apiModuleNotFoundError: No module named 'api'

Also trying relative imports wont work:

Using from ..api.api import function_from_api would result into

PS C:\tmp\test_imports> python .\myproject\tests\test_one.pyTraceback (most recent call last):  File ".\tests\test_one.py", line 1, in <module>    from ..api.api import function_from_apiValueError: attempted relative import beyond top-level package

Steps

  1. Make a setup.py file to the root level directory

The contents for the setup.py would be*

from setuptools import setup, find_packagessetup(name='myproject', version='1.0', packages=find_packages())
  1. Use a virtual environment

If you are familiar with virtual environments, activate one, and skip to the next step. Usage of virtual environments are not absolutely required, but they will really help you out in the long run (when you have more than 1 project ongoing..). The most basic steps are (run in the root folder)

  • Create virtual env
    • python -m venv venv
  • Activate virtual env
    • source ./venv/bin/activate (Linux, macOS) or ./venv/Scripts/activate (Win)

To learn more about this, just Google out "python virtual env tutorial" or similar. You probably never need any other commands than creating, activating and deactivating.

Once you have made and activated a virtual environment, your console should give the name of the virtual environment in parenthesis

PS C:\tmp\test_imports> python -m venv venvPS C:\tmp\test_imports> .\venv\Scripts\activate(venv) PS C:\tmp\test_imports>

and your folder tree should look like this**

.├── myproject│   ├── api│   │   ├── api_key.py│   │   ├── api.py│   │   └── __init__.py│   ├── examples│   │   ├── example_one.py│   │   ├── example_two.py│   │   └── __init__.py│   ├── LICENCE.md│   ├── README.md│   └── tests│       ├── __init__.py│       └── test_one.py├── setup.py└── venv    ├── Include    ├── Lib    ├── pyvenv.cfg    └── Scripts [87 entries exceeds filelimit, not opening dir]
  1. pip install your project in editable state

Install your top level package myproject using pip. The trick is to use the -e flag when doing the install. This way it is installed in an editable state, and all the edits made to the .py files will be automatically included in the installed package.

In the root directory, run

pip install -e . (note the dot, it stands for "current directory")

You can also see that it is installed by using pip freeze

(venv) PS C:\tmp\test_imports> pip install -e .Obtaining file:///C:/tmp/test_importsInstalling collected packages: myproject  Running setup.py develop for myprojectSuccessfully installed myproject(venv) PS C:\tmp\test_imports> pip freezemyproject==1.0
  1. Add myproject. into your imports

Note that you will have to add myproject. only into imports that would not work otherwise. Imports that worked without the setup.py & pip install will work still work fine. See an example below.


Test the solution

Now, let's test the solution using api.py defined above, and test_one.py defined below.

test_one.py

from myproject.api.api import function_from_apidef test_function():    print(function_from_api())if __name__ == '__main__':    test_function()

running the test

(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.pyI am the return value from api.api!

* See the setuptools docs for more verbose setup.py examples.

** In reality, you could put your virtual environment anywhere on your hard disk.


Seven years after

Since I wrote the answer below, modifying sys.path is still a quick-and-dirty trick that works well for private scripts, but there has been several improvements

  • Installing the package (in a virtualenv or not) will give you what you want, though I would suggest using pip to do it rather than using setuptools directly (and using setup.cfg to store the metadata)
  • Using the -m flag and running as a package works too (but will turn out a bit awkward if you want to convert your working directory into an installable package).
  • For the tests, specifically, pytest is able to find the api package in this situation and takes care of the sys.path hacks for you

So it really depends on what you want to do. In your case, though, since it seems that your goal is to make a proper package at some point, installing through pip -e is probably your best bet, even if it is not perfect yet.

Old answer

As already stated elsewhere, the awful truth is that you have to do ugly hacks to allow imports from siblings modules or parents package from a __main__ module. The issue is detailed in PEP 366. PEP 3122 attempted to handle imports in a more rational way but Guido has rejected it one the account of

The only use case seems to be running scripts that happen to be living inside a module's directory, which I've always seen as an antipattern.

(here)

Though, I use this pattern on a regular basis with

# Ugly hack to allow absolute import from the root folder# whatever its name is. Please forgive the heresy.if __name__ == "__main__" and __package__ is None:    from sys import path    from os.path import dirname as dir    path.append(dir(path[0]))    __package__ = "examples"import api

Here path[0] is your running script's parent folder and dir(path[0]) your top level folder.

I have still not been able to use relative imports with this, though, but it does allow absolute imports from the top level (in your example api's parent folder).


Here is another alternative that I insert at top of the Python files in tests folder:

# Path hack.import sys, ossys.path.insert(0, os.path.abspath('..'))