Python-获取根项目结构的路径

问题:Python-获取根项目结构的路径

我在项目根目录中有一个带有配置文件的python项目。在整个项目中,需要使用几个不同的文件来访问配置文件。

所以它看起来是这样的:<ROOT>/configuration.conf <ROOT>/A/a.py<ROOT>/A/B/b.py(当b,a.py访问配置文件)。

在不依赖我所在项目中的哪个文件的情况下,获得项目根目录和配置文件路径的最佳/最简便方法是什么?即不使用../../?可以假设我们知道项目根目录的名称是可以的。

I’ve got a python project with a configuration file in the project root. The configuration file needs to be accessed in a few different files throughout the project.

So it looks something like: <ROOT>/configuration.conf <ROOT>/A/a.py, <ROOT>/A/B/b.py (when b,a.py access the configuration file).

What’s the best / easiest way to get the path to the project root and the configuration file without depending on which file inside the project I’m in? i.e without using ../../? It’s okay to assume that we know the project root’s name.


回答 0

您可以按照Django的方式进行操作:从项目顶层的文件中为项目根定义变量。例如,如果您的项目结构如下所示:

project/
    configuration.conf
    definitions.py
    main.py
    utils.py

在中definitions.py可以定义(这需要import os):

ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) # This is your Project Root

因此,在已知项目根目录的情况下,您可以创建一个指向配置位置的变量(可以在任何位置定义,但逻辑上的位置是将其放置在定义了常量的位置-例如definitions.py):

CONFIG_PATH = os.path.join(ROOT_DIR, 'configuration.conf')  # requires `import os`

然后,你可以很容易地访问与import语句(如中恒(在任何其他文件)utils.pyfrom definitions import CONFIG_PATH

You can do this how Django does it: define a variable to the Project Root from a file that is in the top-level of the project. For example, if this is what your project structure looks like:

project/
    configuration.conf
    definitions.py
    main.py
    utils.py

In definitions.py you can define (this requires import os):

ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) # This is your Project Root

Thus, with the Project Root known, you can create a variable that points to the location of the configuration (this can be defined anywhere, but a logical place would be to put it in a location where constants are defined – e.g. definitions.py):

CONFIG_PATH = os.path.join(ROOT_DIR, 'configuration.conf')  # requires `import os`

Then, you can easily access the constant (in any of the other files) with the import statement (e.g. in utils.py): from definitions import CONFIG_PATH.


回答 1

其他答案建议在项目的顶层使用文件。如果使用pathlib.Pathparent(Python 3.4及更高版本),则不需要这样做。考虑以下目录结构,其中除README.md和以外的所有文件utils.py均被省略。

project
   README.md
|
└───src
      utils.py
|   |   ...
|   ...

utils.py我们定义以下功能。

from pathlib import Path

def get_project_root() -> Path:
    return Path(__file__).parent.parent

现在,在项目的任何模块中,我们都可以按以下方式获取项目根目录。

from src.utils import get_project_root

root = get_project_root()

好处:调用的任何模块get_project_root都可以移动,而无需更改程序行为。只有在utils.py移动模块时,我们才需要更新get_project_root和导入(可以使用重构工具来自动执行此操作)。

Other answers advice to use a file in the top-level of the project. This is not necessary if you use pathlib.Path and parent (Python 3.4 and up). Consider the following directory structure where all files except README.md and utils.py have been omitted.

project
│   README.md
|
└───src
│   │   utils.py
|   |   ...
|   ...

In utils.py we define the following function.

from pathlib import Path

def get_project_root() -> Path:
    return Path(__file__).parent.parent

In any module in the project we can now get the project root as follows.

from src.utils import get_project_root

root = get_project_root()

Benefits: Any module which calls get_project_root can be moved without changing program behavior. Only when the module utils.py is moved we have to update get_project_root and the imports (refactoring tools can be used to automate this).


回答 2

对于我认为您需要的东西,以前的所有解决方案似乎都过于复杂,并且通常不适用于我。下面的单行命令可以满足您的要求:

import os
ROOT_DIR = os.path.abspath(os.curdir)

All the previous solutions seem to be overly complicated for what I think you need, and often didn’t work for me. The following one-line command does what you want:

import os
ROOT_DIR = os.path.abspath(os.curdir)

回答 3

要获取“ root”模块的路径,可以使用:

import os
import sys
os.path.dirname(sys.modules['__main__'].__file__)

但是更有趣的是,如果您在最顶层的模块中有一个配置“对象”,则可以像这样从中读取:

app = sys.modules['__main__']
stuff = app.config.somefunc()

To get the path of the “root” module, you can use:

import os
import sys
os.path.dirname(sys.modules['__main__'].__file__)

But more interestingly if you have an config “object” in your top-most module you could -read- from it like so:

app = sys.modules['__main__']
stuff = app.config.somefunc()

回答 4

实现此目的的标准方法是使用包装中的pkg_resources模块setuptoolssetuptools用于创建可安装的python软件包。

您可以使用pkg_resources以字符串形式返回所需文件的内容,并且可以使用pkg_resources用来获取系统上所需文件的实际路径。

假设您有一个名为的软件包stackoverflow

stackoverflow/
|-- app
|   `-- __init__.py
`-- resources
    |-- bands
    |   |-- Dream\ Theater
    |   |-- __init__.py
    |   |-- King's\ X
    |   |-- Megadeth
    |   `-- Rush
    `-- __init__.py

3 directories, 7 files

现在,假设您要从module访问文件Rush app.run。使用pkg_resources.resouces_filename得到的路径,拉什和pkg_resources.resource_string得到拉什的内容; 因此:

import pkg_resources

if __name__ == "__main__":
    print pkg_resources.resource_filename('resources.bands', 'Rush')
    print pkg_resources.resource_string('resources.bands', 'Rush')

输出:

/home/sri/workspace/stackoverflow/resources/bands/Rush
Base: Geddy Lee
Vocals: Geddy Lee
Guitar: Alex Lifeson
Drums: Neil Peart

这适用于您的python路径中的所有软件包。因此,如果您想知道lxml.etree系统上的位置:

import pkg_resources

if __name__ == "__main__":
    print pkg_resources.resource_filename('lxml', 'etree')

输出:

/usr/lib64/python2.7/site-packages/lxml/etree

关键是您可以使用此标准方法来访问系统上安装的文件(例如pip install xxx或yum -y install python-xxx)以及当前正在使用的模块中的文件。

A standard way to achieve this would be to use the pkg_resources module which is part of the setuptools package. setuptools is used to create an install-able python package.

You can use pkg_resources to return the contents of your desired file as a string and you can use pkg_resources to get the actual path of the desired file on your system.

Let’s say that you have a package called stackoverflow.

stackoverflow/
|-- app
|   `-- __init__.py
`-- resources
    |-- bands
    |   |-- Dream\ Theater
    |   |-- __init__.py
    |   |-- King's\ X
    |   |-- Megadeth
    |   `-- Rush
    `-- __init__.py

3 directories, 7 files

Now let’s say that you want to access the file Rush from a module app.run. Use pkg_resources.resouces_filename to get the path to Rush and pkg_resources.resource_string to get the contents of Rush; thusly:

import pkg_resources

if __name__ == "__main__":
    print pkg_resources.resource_filename('resources.bands', 'Rush')
    print pkg_resources.resource_string('resources.bands', 'Rush')

The output:

/home/sri/workspace/stackoverflow/resources/bands/Rush
Base: Geddy Lee
Vocals: Geddy Lee
Guitar: Alex Lifeson
Drums: Neil Peart

This works for all packages in your python path. So if you want to know where lxml.etree exists on your system:

import pkg_resources

if __name__ == "__main__":
    print pkg_resources.resource_filename('lxml', 'etree')

output:

/usr/lib64/python2.7/site-packages/lxml/etree

The point is that you can use this standard method to access files that are installed on your system (e.g pip install xxx or yum -y install python-xxx) and files that are within the module that you’re currently working on.


回答 5

尝试:

ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

Try:

ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

回答 6

下面的代码返回直到您的项目根目录的路径

import sys
print(sys.path[1])

Below Code Returns the path until your project root

import sys
print(sys.path[1])

回答 7

在解决这个问题之前,我也一直在努力解决这个问题。我认为这是最干净的解决方案。

在您的setup.py中添加“程序包”

setup(
name='package_name'
version='0.0.1'
.
.
.
packages=['package_name']
.
.
.
)

在你的python_script.py中

import pkg_resources
import os

resource_package = pkg_resources.get_distribution(
    'package_name').location
config_path = os.path.join(resource_package,'configuration.conf')

I struggled with this problem too until I came to this solution. This is the cleanest solution in my opinion.

In your setup.py add “packages”

setup(
name='package_name'
version='0.0.1'
.
.
.
packages=['package_name']
.
.
.
)

In your python_script.py

import pkg_resources
import os

resource_package = pkg_resources.get_distribution(
    'package_name').location
config_path = os.path.join(resource_package,'configuration.conf')

回答 8

只是举个例子:我想运行runio.py从内部helper1.py

项目树示例:

myproject_root
- modules_dir/helpers_dir/helper1.py
- tools_dir/runio.py

获取项目根目录:

import os
rootdir = os.path.dirname(os.path.realpath(__file__)).rsplit(os.sep, 2)[0]

构建脚本的路径:

runme = os.path.join(rootdir, "tools_dir", "runio.py")
execfile(runme)

Just an example: I want to run runio.py from within helper1.py

Project tree example:

myproject_root
- modules_dir/helpers_dir/helper1.py
- tools_dir/runio.py

Get project root:

import os
rootdir = os.path.dirname(os.path.realpath(__file__)).rsplit(os.sep, 2)[0]

Build path to script:

runme = os.path.join(rootdir, "tools_dir", "runio.py")
execfile(runme)

回答 9

使用标准的PyCharm项目和虚拟环境(venv)在项目根目录下为我工作。

下面的代码并不是最漂亮的,但是始终能获得项目的根。它从VIRTUAL_ENV环境变量返回venv的完整目录路径,例如/Users/NAME/documents/PROJECT/venv

然后,它最后分割路径/,从而给出一个包含两个元素的数组。第一个元素将是项目路径,例如/Users/NAME/documents/PROJECT

import os

print(os.path.split(os.environ['VIRTUAL_ENV'])[0])

This worked for me using a standard PyCharm project with my virtual environment (venv) under the project root directory.

Code below isnt the prettiest, but consistently gets the project root. It returns the full directory path to venv from the VIRTUAL_ENV environment variable e.g. /Users/NAME/documents/PROJECT/venv

It then splits the path at the last /, giving an array with two elements. The first element will be the project path e.g. /Users/NAME/documents/PROJECT

import os

print(os.path.split(os.environ['VIRTUAL_ENV'])[0])

回答 10

我最近一直在尝试做类似的事情,但我发现这些答案不足以满足我的用例(需要检测项目根目录的分布式库)。主要是我一直在与不同的环境和平台作斗争,但仍然没有找到完全通用的东西。

项目本地代码

我已经看到了这个示例,并在一些地方(例如Django等)使用了该示例。

import os
print(os.path.dirname(os.path.abspath(__file__)))

如此简单,仅当代码片段所在的文件实际上是项目的一部分时才起作用。我们不检索项目目录,而是片段的目录

同样,从应用程序的入口点之外调用时,sys.modules方法会崩溃,特别是我观察到子线程无法在不与’ main ‘模块相关的情况下确定该方法。我已将导入明确地放在一个函数中,以演示从子线程进行的导入,将其移至app.py的顶层将对其进行修复。

app/
|-- config
|   `-- __init__.py
|   `-- settings.py
`-- app.py

app.py

#!/usr/bin/env python
import threading


def background_setup():
    # Explicitly importing this from the context of the child thread
    from config import settings
    print(settings.ROOT_DIR)


# Spawn a thread to background preparation tasks
t = threading.Thread(target=background_setup)
t.start()

# Do other things during initialization

t.join()

# Ready to take traffic

settings.py

import os
import sys


ROOT_DIR = None


def setup():
    global ROOT_DIR
    ROOT_DIR = os.path.dirname(sys.modules['__main__'].__file__)
    # Do something slow

运行此程序会产生属性错误:

>>> import main
>>> Exception in thread Thread-1:
Traceback (most recent call last):
  File "C:\Python2714\lib\threading.py", line 801, in __bootstrap_inner
    self.run()
  File "C:\Python2714\lib\threading.py", line 754, in run
    self.__target(*self.__args, **self.__kwargs)
  File "main.py", line 6, in background_setup
    from config import settings
  File "config\settings.py", line 34, in <module>
    ROOT_DIR = get_root()
  File "config\settings.py", line 31, in get_root
    return os.path.dirname(sys.modules['__main__'].__file__)
AttributeError: 'module' object has no attribute '__file__'

…因此基于线程的解决方案

位置无关

使用与以前相同的应用程序结构,但修改settings.py

import os
import sys
import inspect
import platform
import threading


ROOT_DIR = None


def setup():
    main_id = None
    for t in threading.enumerate():
        if t.name == 'MainThread':
            main_id = t.ident
            break

    if not main_id:
        raise RuntimeError("Main thread exited before execution")

    current_main_frame = sys._current_frames()[main_id]
    base_frame = inspect.getouterframes(current_main_frame)[-1]

    if platform.system() == 'Windows':
        filename = base_frame.filename
    else:
        filename = base_frame[0].f_code.co_filename

    global ROOT_DIR
    ROOT_DIR = os.path.dirname(os.path.abspath(filename))

分解如下:首先,我们要准确地找到主线程的线程ID。threading.main_thread()但是,在Python3.4 +中,线程库已经使用了,每个人都没有使用3.4+,因此我们在所有线程中进行搜索,以查找除ID外的主线程。如果主线程已经退出,则不会在中列出threading.enumerate()RuntimeError()在这种情况下,我们提出一个,直到找到更好的解决方案。

main_id = None
for t in threading.enumerate():
    if t.name == 'MainThread':
        main_id = t.ident
        break

if not main_id:
    raise RuntimeError("Main thread exited before execution")

接下来,我们找到主线程的第一个堆栈框架。使用特定于cPython的函数, sys._current_frames()我们可以获得每个线程当前堆栈框架的字典。然后利用inspect.getouterframes()我们可以检索主线程和第一帧的整个堆栈。current_main_frame = sys._current_frames()[main_id] base_frame = inspect.getouterframes(current_main_frame)[-1]最后,inspect.getouterframes()需要处理Windows和Linux实现之间的差异。使用清理后的文件名,os.path.abspath()然后进行os.path.dirname()清理。

if platform.system() == 'Windows':
    filename = base_frame.filename
else:
    filename = base_frame[0].f_code.co_filename

global ROOT_DIR
ROOT_DIR = os.path.dirname(os.path.abspath(filename))

到目前为止,我已经在Windows的Python2.7和3.6以及WSL的Python3.4上对此进行了测试

I’ve recently been trying to do something similar and I have found these answers inadequate for my use cases (a distributed library that needs to detect project root). Mainly I’ve been battling different environments and platforms, and still haven’t found something perfectly universal.

Code local to project

I’ve seen this example mentioned and used in a few places, Django, etc.

import os
print(os.path.dirname(os.path.abspath(__file__)))

Simple as this is, it only works when the file that the snippet is in is actually part of the project. We do not retrieve the project directory, but instead the snippet’s directory

Similarly, the sys.modules approach breaks down when called from outside the entrypoint of the application, specifically I’ve observed a child thread cannot determine this without relation back to the ‘main‘ module. I’ve explicitly put the import inside a function to demonstrate an import from a child thread, moving it to top level of app.py would fix it.

app/
|-- config
|   `-- __init__.py
|   `-- settings.py
`-- app.py

app.py

#!/usr/bin/env python
import threading


def background_setup():
    # Explicitly importing this from the context of the child thread
    from config import settings
    print(settings.ROOT_DIR)


# Spawn a thread to background preparation tasks
t = threading.Thread(target=background_setup)
t.start()

# Do other things during initialization

t.join()

# Ready to take traffic

settings.py

import os
import sys


ROOT_DIR = None


def setup():
    global ROOT_DIR
    ROOT_DIR = os.path.dirname(sys.modules['__main__'].__file__)
    # Do something slow

Running this program produces an attribute error:

>>> import main
>>> Exception in thread Thread-1:
Traceback (most recent call last):
  File "C:\Python2714\lib\threading.py", line 801, in __bootstrap_inner
    self.run()
  File "C:\Python2714\lib\threading.py", line 754, in run
    self.__target(*self.__args, **self.__kwargs)
  File "main.py", line 6, in background_setup
    from config import settings
  File "config\settings.py", line 34, in <module>
    ROOT_DIR = get_root()
  File "config\settings.py", line 31, in get_root
    return os.path.dirname(sys.modules['__main__'].__file__)
AttributeError: 'module' object has no attribute '__file__'

…hence a threading-based solution

Location independent

Using the same application structure as before but modifying settings.py

import os
import sys
import inspect
import platform
import threading


ROOT_DIR = None


def setup():
    main_id = None
    for t in threading.enumerate():
        if t.name == 'MainThread':
            main_id = t.ident
            break

    if not main_id:
        raise RuntimeError("Main thread exited before execution")

    current_main_frame = sys._current_frames()[main_id]
    base_frame = inspect.getouterframes(current_main_frame)[-1]

    if platform.system() == 'Windows':
        filename = base_frame.filename
    else:
        filename = base_frame[0].f_code.co_filename

    global ROOT_DIR
    ROOT_DIR = os.path.dirname(os.path.abspath(filename))

Breaking this down: First we want to accurately find the thread ID of the main thread. In Python3.4+ the threading library has threading.main_thread() however, everybody doesn’t use 3.4+ so we search through all threads looking for the main thread save it’s ID. If the main thread has already exited, it won’t be listed in the threading.enumerate(). We raise a RuntimeError() in this case until I find a better solution.

main_id = None
for t in threading.enumerate():
    if t.name == 'MainThread':
        main_id = t.ident
        break

if not main_id:
    raise RuntimeError("Main thread exited before execution")

Next we find the very first stack frame of the main thread. Using the cPython specific function sys._current_frames() we get a dictionary of every thread’s current stack frame. Then utilizing inspect.getouterframes() we can retrieve the entire stack for the main thread and the very first frame. current_main_frame = sys._current_frames()[main_id] base_frame = inspect.getouterframes(current_main_frame)[-1] Finally, the differences between Windows and Linux implementations of inspect.getouterframes() need to be handled. Using the cleaned up filename, os.path.abspath() and os.path.dirname() clean things up.

if platform.system() == 'Windows':
    filename = base_frame.filename
else:
    filename = base_frame[0].f_code.co_filename

global ROOT_DIR
ROOT_DIR = os.path.dirname(os.path.abspath(filename))

So far I’ve tested this on Python2.7 and 3.6 on Windows as well as Python3.4 on WSL


回答 11

如果您正在使用anaconda-project,则可以从环境变量-> os.getenv(’PROJECT_ROOT’)查询PROJECT_ROOT。仅当脚本通过anaconda-project run执行时才有效。

如果您不希望脚本由anaconda-project运行,则可以查询正在使用的Python解释器的可执行二进制文件的绝对路径,并将路径字符串提取到envs目录exclusiv。例如:我的conda env的python解释器位于:

/ home / user / project_root / envs / default / bin / python

# You can first retrieve the env variable PROJECT_DIR.
# If not set, get the python interpreter location and strip off the string till envs inclusiv...

if os.getenv('PROJECT_DIR'):
    PROJECT_DIR = os.getenv('PROJECT_DIR')
else:
    PYTHON_PATH = sys.executable
    path_rem = os.path.join('envs', 'default', 'bin', 'python')
    PROJECT_DIR = py_path.split(path_rem)[0]

这仅适用于具有Python项目固定项目结构的conda项目

If you are working with anaconda-project, you can query the PROJECT_ROOT from the environment variable –> os.getenv(‘PROJECT_ROOT’). This works only if the script is executed via anaconda-project run .

If you do not want your script run by anaconda-project, you can query the absolute path of the executable binary of the Python interpreter you are using and extract the path string up to the envs directory exclusiv. For example: The python interpreter of my conda env is located at:

/home/user/project_root/envs/default/bin/python

# You can first retrieve the env variable PROJECT_DIR.
# If not set, get the python interpreter location and strip off the string till envs inclusiv...

if os.getenv('PROJECT_DIR'):
    PROJECT_DIR = os.getenv('PROJECT_DIR')
else:
    PYTHON_PATH = sys.executable
    path_rem = os.path.join('envs', 'default', 'bin', 'python')
    PROJECT_DIR = py_path.split(path_rem)[0]

This works only with conda-project with fixed project structure of a anaconda-project


回答 12

我使用../方法来获取当前项目路径。

例如:Project1-D:\ projects

src

配置文件

Configuration.cfg

路径=“ ../ src / ConfigurationFiles / Configuration.cfg”

I used the ../ method to fetch the current project path.

Example: Project1 — D:\projects

src

ConfigurationFiles

Configuration.cfg

Path=”../src/ConfigurationFiles/Configuration.cfg”


回答 13

在撰写本文时,没有其他解决方案是非常独立的。它们取决于环境变量或模块在包装结构中的位置。“ Django”解决方案的最高答案是后者的受害者,因为它需要相对导入。它还具有必须在顶层修改模块的缺点。

这应该是查找顶级软件包目录路径的正确方法:

import sys
import os

root_name, _, _ = __name__.partition('.')
root_module = sys.modules[root_name]
root_dir = os.path.dirname(root_module.__file__)

config_path = os.path.join(root_dir, 'configuration.conf')

它通过将其中包含的点分字符串中的第一个组件__name__用作键并将其用作sys.modules返回顶部包的模块对象的键来工作。其__file__属性包含/__init__.py使用修剪后我们想要的路径os.path.dirname()

该解决方案是独立的。它可以在包的任何模块中的任何位置运行,包括在顶级__init__.py文件中。

At the time of writing, none of the other solutions are very self-contained. They depend either on an environment variable or the position of the module in the package structure. The top answer with the ‘Django’ solution falls victim to the latter by requiring a relative import. It also has the disadvantage of having to modify a module at the top level.

This should be the correct approach for finding the directory path of the top-level package:

import sys
import os

root_name, _, _ = __name__.partition('.')
root_module = sys.modules[root_name]
root_dir = os.path.dirname(root_module.__file__)

config_path = os.path.join(root_dir, 'configuration.conf')

It works by taking the first component in the dotted string contained in __name__ and using it as a key in sys.modules which returns the module object of the top-level package. Its __file__ attribute contains the path we want after trimming off /__init__.py using os.path.dirname().

This solution is self-contained. It works anywhere in any module of the package, including in the top-level __init__.py file.


回答 14

我必须实现一个自定义解决方案,因为它没有您想象的那么简单。我的解决方案基于堆栈跟踪检查(inspect.stack())+ sys.path,无论在其中调用该函数的python模块的位置还是在解释器的位置都可以正常工作(我尝试通过在PyCharm中,在诗歌外壳等中运行它的方式来尝试… )。这是带有注释的完整实现:

def get_project_root_dir() -> str:
    """
    Returns the name of the project root directory.

    :return: Project root directory name
    """

    # stack trace history related to the call of this function
    frame_stack: [FrameInfo] = inspect.stack()

    # get info about the module that has invoked this function
    # (index=0 is always this very module, index=1 is fine as long this function is not called by some other
    # function in this module)
    frame_info: FrameInfo = frame_stack[1]

    # if there are multiple calls in the stacktrace of this very module, we have to skip those and take the first
    # one which comes from another module
    if frame_info.filename == __file__:
        for frame in frame_stack:
            if frame.filename != __file__:
                frame_info = frame
                break

    # path of the module that has invoked this function
    caller_path: str = frame_info.filename

    # absolute path of the of the module that has invoked this function
    caller_absolute_path: str = os.path.abspath(caller_path)

    # get the top most directory path which contains the invoker module
    paths: [str] = [p for p in sys.path if p in caller_absolute_path]
    paths.sort(key=lambda p: len(p))
    caller_root_path: str = paths[0]

    if not os.path.isabs(caller_path):
        # file name of the invoker module (eg: "mymodule.py")
        caller_module_name: str = Path(caller_path).name

        # this piece represents a subpath in the project directory
        # (eg. if the root folder is "myproject" and this function has ben called from myproject/foo/bar/mymodule.py
        # this will be "foo/bar")
        project_related_folders: str = caller_path.replace(os.sep + caller_module_name, '')

        # fix root path by removing the undesired subpath
        caller_root_path = caller_root_path.replace(project_related_folders, '')

    dir_name: str = Path(caller_root_path).name

    return dir_name

I had to implement a custom solution because it’s not as simple as you might think. My solution is based on stack trace inspection (inspect.stack()) + sys.path and is working fine no matter the location of the python module in which the function is invoked nor the interpreter (I tried by running it in PyCharm, in a poetry shell and other…). This is the full implementation with comments:

def get_project_root_dir() -> str:
    """
    Returns the name of the project root directory.

    :return: Project root directory name
    """

    # stack trace history related to the call of this function
    frame_stack: [FrameInfo] = inspect.stack()

    # get info about the module that has invoked this function
    # (index=0 is always this very module, index=1 is fine as long this function is not called by some other
    # function in this module)
    frame_info: FrameInfo = frame_stack[1]

    # if there are multiple calls in the stacktrace of this very module, we have to skip those and take the first
    # one which comes from another module
    if frame_info.filename == __file__:
        for frame in frame_stack:
            if frame.filename != __file__:
                frame_info = frame
                break

    # path of the module that has invoked this function
    caller_path: str = frame_info.filename

    # absolute path of the of the module that has invoked this function
    caller_absolute_path: str = os.path.abspath(caller_path)

    # get the top most directory path which contains the invoker module
    paths: [str] = [p for p in sys.path if p in caller_absolute_path]
    paths.sort(key=lambda p: len(p))
    caller_root_path: str = paths[0]

    if not os.path.isabs(caller_path):
        # file name of the invoker module (eg: "mymodule.py")
        caller_module_name: str = Path(caller_path).name

        # this piece represents a subpath in the project directory
        # (eg. if the root folder is "myproject" and this function has ben called from myproject/foo/bar/mymodule.py
        # this will be "foo/bar")
        project_related_folders: str = caller_path.replace(os.sep + caller_module_name, '')

        # fix root path by removing the undesired subpath
        caller_root_path = caller_root_path.replace(project_related_folders, '')

    dir_name: str = Path(caller_root_path).name

    return dir_name

回答 15

这里有很多答案,但我找不到涵盖所有情况的简单方法,因此也请提出我的解决方案:

import pathlib
import os

def get_project_root():
    """
    There is no way in python to get project root. This function uses a trick.
    We know that the function that is currently running is in the project.
    We know that the root project path is in the list of PYTHONPATH
    look for any path in PYTHONPATH list that is contained in this function's path
    Lastly we filter and take the shortest path because we are looking for the root.
    :return: path to project root
    """
    apth = str(pathlib.Path().absolute())
    ppth = os.environ['PYTHONPATH'].split(':')
    matches = [x for x in ppth if x in apth]
    project_root = min(matches, key=len)
    return project_root

There are many answers here but I couldn’t find something simple that covers all cases so allow me to suggest my solution too:

import pathlib
import os

def get_project_root():
    """
    There is no way in python to get project root. This function uses a trick.
    We know that the function that is currently running is in the project.
    We know that the root project path is in the list of PYTHONPATH
    look for any path in PYTHONPATH list that is contained in this function's path
    Lastly we filter and take the shortest path because we are looking for the root.
    :return: path to project root
    """
    apth = str(pathlib.Path().absolute())
    ppth = os.environ['PYTHONPATH'].split(':')
    matches = [x for x in ppth if x in apth]
    project_root = min(matches, key=len)
    return project_root