Reputation: 4681
I've tried reading through questions about sibling imports and even the package documentation, but I've yet to find an answer.
With the following structure:
├── LICENSE.md
├── README.md
├── api
│ ├── __init__.py
│ ├── api.py
│ └── api_key.py
├── examples
│ ├── __init__.py
│ ├── example_one.py
│ └── example_two.py
└── tests
│ ├── __init__.py
│ └── test_one.py
How can the scripts in the examples
and tests
directories import from the
api
module and be run from the commandline?
Also, I'd like to avoid the ugly sys.path.insert
hack for every file. Surely
this can be done in Python, right?
Upvotes: 397
Views: 252334
Reputation: 33730
There are plenty of sys.path.append
-hacks available, but I found an alternative way of solving the problem in hand.
packaged_stuff
)pyproject.toml
file to describe your package (see minimal pyproject.toml
below)pip install -e <myproject_folder>
from packaged_stuff.modulename import function_name
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\
.
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!'
from api.api import function_from_api
def test_function():
print(function_from_api())
if __name__ == '__main__':
test_function()
PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
File ".\myproject\tests\test_one.py", line 1, in <module>
from api.api import function_from_api
ModuleNotFoundError: No module named 'api'
Using from ..api.api import function_from_api
would result into
PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
File ".\tests\test_one.py", line 1, in <module>
from ..api.api import function_from_api
ValueError: attempted relative import beyond top-level package
(previously people used a setup.py file)
The contents for a minimal pyproject.toml
would be*
[project]
name = "myproject"
version = "0.1.0"
description = "My small project"
[build-system]
build-backend = "flit_core.buildapi"
requires = ["flit_core >=3.2,<4"]
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)
python -m venv venv
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 venv
PS 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
├── pyproject.toml
└── venv
├── Include
├── Lib
├── pyvenv.cfg
└── Scripts [87 entries exceeds filelimit, not opening dir]
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. Using pyproject.toml and -e flag requires pip >= 21.3
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
Obtaining file:///home/user/projects/myproject
Installing build dependencies ... done
Checking if build backend supports build_editable ... done
Getting requirements to build editable ... done
Preparing editable metadata (pyproject.toml) ... done
Building wheels for collected packages: myproj
Building editable for myproj (pyproject.toml) ... done
Created wheel for myproj: filename=myproj-0.1.0-py2.py3-none-any.whl size=903 sha256=f19858b080d4e770c2a172b9a73afcad5f33f4c43c86e8eb9bdacbe50a627064
Stored in directory: /tmp/pip-ephem-wheel-cache-qohzx1u0/wheels/55/5f/e4/507fdeb40cdef333e3e0a8c50c740a430b8ce84cbe17ae5875
Successfully built myproject
Installing collected packages: myproject
Successfully installed myproject-0.1.0
(venv) PS C:\tmp\test_imports> pip freeze
myproject==0.1.0
myproject.
into your importsNote that you will have to add myproject.
only into imports that would not work otherwise. Imports that worked without the pyproject.toml
& pip install
will work still work fine. See an example below.
Now, let's test the solution using api.py
defined above, and test_one.py
defined below.
from myproject.api.api import function_from_api
def test_function():
print(function_from_api())
if __name__ == '__main__':
test_function()
(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!
* here using flit as build backend. Other alternatives exist.
** In reality, you could put your virtual environment anywhere on your hard disk.
Upvotes: 439
Reputation: 2891
pip install -e
:TL;DR: A script(usually an entry point) can only import
anything the same or below its level.
Consider this hierarchy, as recommended by an answer from Relative imports in Python 3:
MyProject
├── src
│ ├── bot
│ │ ├── __init__.py
│ │ ├── main.py
│ │ └── sib1.py
│ └── mod
│ ├── __init__.py
│ └── module1.py
└── main.py
To run our program from the starting point with the simple command python main.py
, we use absolute import (no leading dot(s)) in main.py
here:
from src.bot import main
if __name__ == '__main__':
main.magic_tricks()
The content of bot/main.py
, which takes advantage of explicit relative imports to show what we're importing, looks like this:
from .sib1 import my_drink # Both are explicit-relative-imports.
from ..mod.module1 import relative_magic
def magic_tricks():
# Using sub-magic
relative_magic(in=["newbie", "pain"], advice="cheer_up")
my_drink()
# Do your work
...
These are the reasonings:
main.py
, this way we can run our program by simply python main.py
.sys.path
to resolve packages for us, but this also means that the package we want to import can probably be superseded by any other package of the same name due to the ordering of paths in sys.path
e.g. try import test
.from ..mod
syntax makes it very clear about "we're importing our own local package".from ..mod
part means that it will go up one level to MyProject/src
.main.py
script next to the root of all your packages MyProject/src
, and use absolute import in python main.py
to import anything. No one will create a package named src
.python -m ...
.src/
as a script?Then you should use the syntax python -m
and take a look at my other post: ModuleNotFoundError: No module named 'sib1'
Upvotes: 14
Reputation: 497
I made a sample project to demonstrate how I handled this, which is indeed another sys.path hack as indicated above. Python Sibling Import Example, which relies on:
if __name__ == '__main__': import os import sys sys.path.append(os.getcwd())
This seems to be pretty effective so long as your working directory remains at the root of the Python project.
Upvotes: 0
Reputation: 1247
If you're using pytest then the pytest docs describe a method of how to reference source packages from a separate test package.
The suggested project directory structure is:
setup.py
src/
mypkg/
__init__.py
app.py
view.py
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
Contents of the setup.py
file:
from setuptools import setup, find_packages
setup(name="PACKAGENAME", packages=find_packages())
Install the packages in editable mode:
pip install -e .
The pytest article references this blog post by Ionel Cristian Mărieș.
Upvotes: 1
Reputation: 310
I wanted to comment on the solution provided by np8 but I don't have enough reputation so I'll just mention that you can create a setup.py file exactly as they suggested, and then do pipenv install --dev -e .
from the project root directory to turn it into an editable dependency. Then your absolute imports will work e.g. from api.api import foo
and you don't have to mess around with system-wide installations.
Upvotes: 1
Reputation: 4485
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
setup.cfg
to store the metadata)-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).sys.path
hacks for youSo 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.
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).
Upvotes: 121
Reputation: 2888
Here is another alternative that I insert at top of the Python files in tests
folder:
# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Upvotes: 50
Reputation: 414069
You don't need and shouldn't hack sys.path
unless it is necessary and in this case it is not. Use:
import api.api_key # in tests, examples
Run from the project directory: python -m tests.test_one
.
You should probably move tests
(if they are api's unittests) inside api
and run python -m api.test
to run all tests (assuming there is __main__.py
) or python -m api.test.test_one
to run test_one
instead.
You could also remove __init__.py
from examples
(it is not a Python package) and run the examples in a virtualenv where api
is installed e.g., pip install -e .
in a virtualenv would install inplace api
package if you have proper setup.py
.
Upvotes: 47
Reputation: 9684
For siblings package imports, you can use either the insert or the append method of the [sys.path][2] module:
if __name__ == '__main__' and if __package__ is None:
import sys
from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
import api
This will work if you are launching your scripts as follows:
python examples/example_one.py
python tests/test_one.py
On the other hand, you can also use the relative import:
if __name__ == '__main__' and if __package__ is not None:
import ..api.api
In this case you will have to launch your script with the '-m' argument (note that, in this case, you must not give the '.py' extension):
python -m packageName.examples.example_one
python -m packageName.tests.test_one
Of course, you can mix the two approaches, so that your script will work no matter how it is called:
if __name__ == '__main__':
if __package__ is None:
import sys
from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
import api
else:
import ..api.api
Upvotes: 5
Reputation: 1362
Just in case someone using Pydev on Eclipse end up here: you can add the sibling's parent path (and thus the calling module's parent) as an external library folder using Project->Properties and setting External Libraries under the left menu Pydev-PYTHONPATH. Then you can import from your sibling, e. g. from sibling import some_class
.
Upvotes: 1
Reputation: 135
I don't yet have the comprehension of Pythonology necessary to see the intended way of sharing code amongst unrelated projects without a sibling/relative import hack. Until that day, this is my solution. For examples
or tests
to import stuff from ..\api
, it would look like:
import sys.path
import os.path
# Import from sibling directory ..\api
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..")
import api.api
import api.api_key
Upvotes: 11
Reputation: 28174
You need to look to see how the import statements are written in the related code. If examples/example_one.py
uses the following import statement:
import api.api
...then it expects the root directory of the project to be in the system path.
The easiest way to support this without any hacks (as you put it) would be to run the examples from the top level directory, like this:
PYTHONPATH=$PYTHONPATH:. python examples/example_one.py
Upvotes: 2