兄弟包导入
我尝试阅读有关兄弟导入的问题,甚至 包文档,但我还没有找到答案。
结构如下:
├── 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
examples
和 tests
目录下的脚本如何从 api
模块并从命令行运行?
另外,我想避免对每个文件进行丑陋的 sys.path.insert
hack。一定 这可以用 Python 完成,对吗?
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 theapi
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?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(12)
厌倦了 sys.path 黑客攻击?
有很多可用的 sys.path.append -hacks,但我找到了解决当前问题的替代方法。
摘要
packaged_stuff
)pyproject.toml
文件来描述您的包(请参阅下面的最小pyproject.toml
)pip install -e
以可编辑状态安装软件包from packaged_stuff.modulename import function_name
设置
起点是您的文件结构已提供,包装在名为
myproject
的文件夹中。我将
.
称为根文件夹,在我的示例中,它位于C:\tmp\test_imports\
。api.py
作为测试用例,让我们使用以下 ./api/api.py
test_one.py
尝试运行 test_one:
同时尝试相对导入也不起作用:
使用
from ..api.api import function_from_api
将导致步骤
1) 在根级别目录中创建一个 pyproject.toml 文件
(以前人们使用 setup.py 文件)
最小
pyproject.toml
的内容将是*2) 使用虚拟环境
如果您熟悉虚拟环境,请激活一个虚拟环境,然后跳到下一步。使用虚拟环境并不是绝对必需的,但它们会从长远来看,确实可以帮助您(当您有超过 1 个项目正在进行时......)。最基本的步骤是(在根文件夹中运行)
python -m venv venv
源 ./venv/bin/activate
(Linux、macOS)或./venv/Scripts/activate
(Win)要了解更多信息,只需 Google 出“python virtual env 教程”或类似内容。除了创建、激活和停用之外,您可能不需要任何其他命令。
创建并激活虚拟环境后,您的控制台应在括号中给出虚拟环境的名称
,您的文件夹树应如下所示**
3) pip install your project in editable state
安装您的顶级包
myproject
使用pip
。诀窍是在安装时使用-e
标志。这样它就会以可编辑状态安装,并且对 .py 文件所做的所有编辑都将自动包含在已安装的包中。 使用 pyproject.toml 和 -e 标志需要 pip >= 21.3在根目录中,运行
pip install -e .
(注意点,它代表“当前目录” ")您还可以看到它是使用
pip freeze
安装的4) 将
myproject.
添加到您的导入中请注意,您必须添加
myproject.
代码>仅进入导入否则无法工作。无需pyproject.toml
& 即可导入pip install
仍然可以正常工作。请参阅下面的示例。测试解决方案
现在,让我们使用上面定义的
api.py
和下面定义的test_one.py
来测试该解决方案。test_one.py
在这里使用 flit 作为构建后端运行测试
。还存在其他替代方案。
** 实际上,您可以将虚拟环境放置在硬盘上的任何位置。
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
packaged_stuff
)pyproject.toml
file to describe your package (see minimalpyproject.toml
below)pip install -e <myproject_folder>
from packaged_stuff.modulename import function_name
Setup
The starting point is the file structure you have provided, wrapped in a folder called
myproject
.I will call the
.
the root folder, and in my example case it is located atC:\tmp\test_imports\
.api.py
As a test case, let's use the following ./api/api.py
test_one.py
Try to run test_one:
Also trying relative imports wont work:
Using
from ..api.api import function_from_api
would result intoSteps
1) Make a pyproject.toml file to the root level directory
(previously people used a setup.py file)
The contents for a minimal
pyproject.toml
would be*2) 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)
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
and your folder tree should look like this**
3) pip install your project in editable state
Install your top level package
myproject
usingpip
. 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.3In 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
4) Add
myproject.
into your importsNote that you will have to add
myproject.
only into imports that would not work otherwise. Imports that worked without thepyproject.toml
&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, andtest_one.py
defined below.test_one.py
running the test
* here using flit as build backend. Other alternatives exist.
** In reality, you could put your virtual environment anywhere on your hard disk.
自从我写下答案七年后
,修改
sys.path
仍然是一个快速而肮脏的技巧,对于私有脚本效果很好,但已经有了一些改进setup.cfg
来存储元数据)-m
标志 并且作为包运行也可以(但如果您想将工作目录转换为可安装包,则会有点尴尬)。sys. path
hacks for you所以这实际上取决于你想做什么。不过,就您而言,由于您的目标似乎是在某个时候制作一个合适的软件包,因此通过 pip -e 安装可能是您最好的选择,即使它还不完美。
旧答案
正如其他地方已经指出的,可怕的事实是,您必须进行丑陋的黑客攻击才能允许从
__main__
模块从兄弟模块或父包导入。 PEP 366 中详细介绍了该问题。 PEP 3122 试图以更合理的方式处理导入,但 Guido 拒绝了一个帐户(此处)
不过,我在定期使用
这里
path[0]
是您正在运行的脚本的父文件夹,dir(path[0])
是您的顶级文件夹。不过,我仍然无法使用相对导入,但它确实允许从顶层(在您的示例
api
的父文件夹中)进行绝对导入。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 improvementssetup.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.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(here)
Though, I use this pattern on a regular basis with
Here
path[0]
is your running script's parent folder anddir(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).这是我在
tests
文件夹中的 Python 文件顶部插入的另一个替代方案:Here is another alternative that I insert at top of the Python files in
tests
folder:您不需要也不应该破解
sys.path
除非有必要,但在本例中则不然。使用:从项目目录运行:
python -mtests.test_one
。您可能应该将
tests
(如果它们是 api 的单元测试)移到api
内并运行python -m api.test
来运行所有测试(假设有是__main__.py
) 或python -m api.test.test_one
来运行test_one
。您还可以从
examples
(它不是 Python 包)中删除__init__.py
并在安装了api
的 virtualenv 中运行示例,例如如果您有正确的setup.py
,virtualenv 中的pip install -e .
将安装就地api
包。You don't need and shouldn't hack
sys.path
unless it is necessary and in this case it is not. Use:Run from the project directory:
python -m tests.test_one
.You should probably move
tests
(if they are api's unittests) insideapi
and runpython -m api.test
to run all tests (assuming there is__main__.py
) orpython -m api.test.test_one
to runtest_one
instead.You could also remove
__init__.py
fromexamples
(it is not a Python package) and run the examples in a virtualenv whereapi
is installed e.g.,pip install -e .
in a virtualenv would install inplaceapi
package if you have propersetup.py
.对于 2023 年的读者:如果您对
pip install -e
没有信心:TL;DR:脚本(通常是入口点)只能
导入
任何相同或低于其水平。考虑这个层次结构,正如 Python 3 中的相对导入的答案所建议的那样:
从起点运行我们的程序
python main.py
,我们在main.py
中使用绝对导入(无前导点):简单的命令
bot/main.py
,它利用显式相对导入来显示我们正在导入的内容,如下所示:原因如下:
main.py
,这样我们就可以通过python main.py
来运行我们的程序.sys.path
为我们解析包,但这也意味着我们想要导入的包可能会被任何其他包所取代由于sys.path
中路径的顺序,名称相同,例如尝试import test
。from ..mod
语法非常清楚地表明“我们正在导入我们自己的本地包”。from ..mod
部分意味着它将上升一级到MyProject/src
。结论
main.py
脚本放在所有包MyProject/src
的根目录旁边,并在python main 中使用绝对导入.py
导入任何内容。没有人会创建名为src
的包。附录:有关将
src/
下的任何文件作为脚本运行的更多信息?那么你应该使用语法
python -m
并看看我的另一篇文章: ModuleNotFoundError: No module named 'sib1'For readers in 2023: If you're not confident with
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:
To run our program from the starting point with the simple command
python main.py
, we use absolute import (no leading dot(s)) inmain.py
here:The content of
bot/main.py
, which takes advantage of explicit relative imports to show what we're importing, looks like this:These are the reasonings:
main.py
, this way we can run our program by simplypython 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 insys.path
e.g. tryimport 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 toMyProject/src
.Conclusion
main.py
script next to the root of all your packagesMyProject/src
, and use absolute import inpython main.py
to import anything. No one will create a package namedsrc
.python -m ...
.Appendix: More about running any file under
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'我还没有对 Pythonology 的理解,无法了解在不相关的项目之间共享代码而无需兄弟/相对导入 hack 的预期方式。直到那一天,这就是我的解决方案。对于从
..\api
导入内容的examples
或tests
,它看起来像: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
ortests
to import stuff from..\api
, it would look like:对于兄弟包导入,您可以使用 [sys.path][2] 模块的 insert 或 append 方法:
这将如果您按如下方式启动脚本,则可以工作:
另一方面,您也可以使用相对导入:
在这种情况下,您必须使用 '-m' 参数(请注意,在这种情况下,您不能给出 '.py' 扩展名):
当然,您可以混合使用两者方法,以便您的脚本无论如何调用都可以工作:
For siblings package imports, you can use either the insert or the append method of the [sys.path][2] module:
This will work if you are launching your scripts as follows:
On the other hand, you can also use the relative import:
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):
Of course, you can mix the two approaches, so that your script will work no matter how it is called:
你需要看看相关代码中的导入语句是如何编写的。如果
examples/example_one.py
使用以下导入语句:...那么它期望项目的根目录位于系统路径中。
在没有任何黑客攻击的情况下支持这一点的最简单方法(如您所说)是从顶级目录运行示例,如下所示:
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:...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:
以防万一有人在 Eclipse 上使用 Pydev 到这里:您可以使用 Project->Properties 并设置 将同级的父路径(以及调用模块的父路径)添加为外部库文件夹左侧菜单下的外部库Pydev-PYTHONPATH。然后你可以从你的同级导入,例如
fromsibling import some_class
。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
.我想对 np8 提供的 解决方案发表评论,但我没有足够的声誉所以我只想提一下,您可以完全按照他们的建议创建一个 setup.py 文件,然后从项目根目录执行
pipenv install --dev -e .
将其变成可编辑的依赖项。然后你的绝对导入将起作用,例如from api.api import foo
并且你不必搞乱系统范围的安装。文档
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.Documentation
如果您使用 pytest,则 pytest 文档 描述了如何从单独的测试包引用源包的方法。
建议的项目目录结构是:
setup.py
文件的内容:以可编辑模式安装软件包:
pytest 文章引用Ionel Cristian Mărieş 的这篇博文。
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:
Contents of the
setup.py
file:Install the packages in editable mode:
The pytest article references this blog post by Ionel Cristian Mărieș.
我制作了一个示例项目来演示我如何处理这个问题,这确实是另一个 sys.path hack,如上所述。 Python 同级导入示例,依赖于:
if __name__ == '__main__': import os import sys sys.path.append(os.getcwd())
只要您的工作目录保留在 Python 项目的根目录下,这似乎就非常有效。
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.