Python: obtiene la ruta de la estructura del proyecto raíz

127

Tengo un proyecto de Python con un archivo de configuración en la raíz del proyecto. Es necesario acceder al archivo de configuración en algunos archivos diferentes a lo largo del proyecto.

Por lo que se ve algo como: <ROOT>/configuration.conf <ROOT>/A/a.py, <ROOT>/A/B/b.py(cuando b, acceso a.py el archivo de configuración).

¿Cuál es la forma mejor / más fácil de obtener la ruta a la raíz del proyecto y el archivo de configuración sin depender del archivo dentro del proyecto en el que estoy? es decir, sin usar ../../? Está bien suponer que conocemos el nombre de la raíz del proyecto.

Shookie
fuente
no <ROOT>/__init__.pyexiste?
mgilson
O su archivo de configuración es un módulo de Python, y puede acceder fácilmente a él solo con una declaración de importación, o no es un módulo de Python y debe colocarlo en una ubicación conocida. Por ejemplo, $ HOME / .my_project / my_project.conf.
John Smith Opcional
@JohnSmithOptional: es un archivo JSON. Necesito poder acceder a él usando la ruta. Si. Todas las carpetas lo incluyen.
Shookie
_ Está bien suponer que conocemos el nombre de la raíz del proyecto. ¿Eso significa que conoce la ruta al proyecto? Entonces, ¿no es solo os.path.join (nombre_root_conocido, "configuration.conf")?
tdelaney
Si es una configuración de usuario, generalmente usaría algo como os.path.expanduser('~/.myproject/myproject.conf'). Funciona en Unix y Windows.
John Smith Opcional

Respuestas:

157

Puede hacer esto como lo hace Django: defina una variable para la raíz del proyecto desde un archivo que se encuentra en el nivel superior del proyecto. Por ejemplo, si así es como se ve la estructura de su proyecto:

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

En definitions.pypuede definir (esto requiere import os):

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

Por lo tanto, con la raíz del proyecto conocida, puede crear una variable que apunte a la ubicación de la configuración (esto se puede definir en cualquier lugar, pero un lugar lógico sería colocarlo en una ubicación donde se definen las constantes, por ejemplo definitions.py):

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

A continuación, se puede acceder fácilmente a la constante (en cualquiera de los otros archivos) con la declaración de importación (por ejemplo, en utils.py): from definitions import CONFIG_PATH.

jrd1
fuente
1
Para incluir el archivo definitions.py como ese, ¿será necesario agregar un __init__.pyarchivo al directorio raíz del proyecto también? ¿Debería ser eso correcto? Acabo de comenzar con Python y no estoy seguro de las mejores prácticas. Gracias.
akskap
3
@akskap: No, __init__.pyno se requerirá un archivo, ya que ese archivo solo se requiere al definir paquetes: los __init__.pyarchivos son necesarios para que Python trate los directorios como si fueran paquetes; Esto se hace para evitar que los directorios con un nombre común, como cadena, oculten involuntariamente módulos válidos que aparecen más adelante en la ruta de búsqueda del módulo. En el caso más simple, __init__.pypuede ser simplemente un archivo vacío, pero también puede ejecutar el código de inicialización del paquete o establecer la __all__variable, que se describe más adelante. Ver: docs.python.org/3/tutorial/modules.html#packages
jrd1
Soy curioso, en cuanto al estilo, si es aceptable o desaprobado agregar estas definiciones al __init.py__paquete raíz. Guardaría la creación de otro archivo, además de permitir la sintaxis más agradable de from root_pack import ROOT_DIR, CONFIG_PATH.
Johndt6
@ Johndt6: la convención es mantener el __init__.pyvacío, pero eso no es estrictamente cierto (es una convención después de todo). Consulte esto para obtener más información: stackoverflow.com/questions/2361124/using-init-py
jrd1
1
@JavNoor: no - en el ejemplo usted ha citado, os.path.abspathestá llamando a una cadena, '__file__'. Recuerde que en __file__realidad es un atributo de importación que se define para los módulos de Python. En este caso, __file__devolverá la ruta desde la que se carga el módulo. Lea más aquí (consulte la sección de módulos): docs.python.org/3/reference/datamodel.html
jrd1
61

Otras respuestas aconsejan utilizar un archivo en el nivel superior del proyecto. Esto no es necesario si usa pathlib.Pathy parent(Python 3.4 y superior). Considere la siguiente estructura de directorio donde se han omitido todos los archivos excepto README.mdy utils.py.

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

En utils.pydefinimos la siguiente función.

from pathlib import Path

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

En cualquier módulo del proyecto, ahora podemos obtener la raíz del proyecto de la siguiente manera.

from src.utils import get_project_root

root = get_project_root()

Beneficios : Cualquier módulo que llame get_project_rootse puede mover sin cambiar el comportamiento del programa. Solo cuando utils.pyse mueve el módulo , tenemos que actualizar get_project_rooty las importaciones (se pueden utilizar herramientas de refactorización para automatizar esto).

RikH
fuente
2
Cualquier módulo que esté en la raíz. Llamar a src.utils desde fuera de la raíz no debería funcionar. ¿Me equivoco?
aerijman
el nombre ' archivo ' no está definido, ¿por qué?
Luk Aron
26

Todas las soluciones anteriores parecen ser demasiado complicadas para lo que creo que necesita y, a menudo, no me funcionaron. El siguiente comando de una línea hace lo que quiere:

import os
ROOT_DIR = os.path.abspath(os.curdir)
Martim
fuente
3
Ponlo en config.py, en la raíz del directorio, .. ¡bamn! Tienes un singleton.
swdev
2
Este método asume que ejecuta la aplicación desde dentro de la ruta que existe. Muchos "usuarios" tienen un icono en el que hacen clic desde un escritorio o pueden ejecutar la aplicación desde otro directorio por completo.
DevPlayer
23

Para obtener la ruta del módulo "raíz", puede usar:

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

Pero lo más interesante es que si tiene un "objeto" de configuración en su módulo superior, podría leerlo así:

app = sys.modules['__main__']
stuff = app.config.somefunc()
DevPlayer
fuente
1
Aquí osno está disponible de forma predeterminada. Necesita importar os. Entonces, agregar la línea import osharía que la respuesta fuera más completa.
Md. Abu Nafee Ibna Zahid
5
Esto proporciona el directorio que contiene el script que se ejecutó. Por ejemplo, cuando se ejecuta python3 -m topmodule.submodule.script, dará en /path/to/topmodule/submodulelugar de /path/to/topmodule.
danijar
14

Una forma estándar de lograr esto sería utilizar el pkg_resourcesmódulo que forma parte del setuptoolspaquete. setuptoolsse utiliza para crear un paquete de Python instalable.

Puede usar pkg_resourcespara devolver el contenido de su archivo deseado como una cadena y puede usar pkg_resourcespara obtener la ruta real del archivo deseado en su sistema.

Digamos que tiene un paquete llamado stackoverflow.

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

3 directories, 7 files

Ahora digamos que desea acceder al archivo Rush desde un módulo app.run. Úselo pkg_resources.resouces_filenamepara obtener el camino a Rush y pkg_resources.resource_stringpara obtener el contenido de Rush; así:

import pkg_resources

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

La salida:

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

Esto funciona para todos los paquetes en su ruta de Python. Entonces, si desea saber dónde lxml.etreeexiste en su sistema:

import pkg_resources

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

salida:

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

El punto es que puede usar este método estándar para acceder a los archivos que están instalados en su sistema (por ejemplo, pip install xxx o yum -y install python-xxx) y archivos que están dentro del módulo en el que está trabajando actualmente.

musaraña
fuente
1
¡Me gusta tu elección de banda!
dylan_fan
3

Tratar:

ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
Harry
fuente
1
Esto es exactamente lo que necesitaba. Solución simple, funciona para mí porque mi estructura era root-> config-> conf.py Quería definir la raíz del proyecto en conf.py y la raíz estaba exactamente dos niveles más arriba de ese archivo.
Daniyal Arshad
3

Debajo del código Devuelve la ruta hasta la raíz de su proyecto

import sys
print(sys.path[1])
Arpan Saini
fuente
Buen consejo! Me pregunto por qué nadie votó a favor de tu respuesta excepto yo: P
daveoncode
Gracias Daveon Realmente aprecio eso !!
Arpan Saini
Desafortunadamente no es eso, simple: P ... eche un vistazo a mi solución completa: stackoverflow.com/a/62510836/267719
daveoncode
2

También luché con este problema hasta que llegué a esta solución. Esta es la solución más limpia en mi opinión.

En su setup.py agregue "paquetes"

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

En su 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')
Chico
fuente
Usar un entorno virtual e instalar el paquete con python3 setup.py installél ya no apuntaba a la carpeta del código fuente, sino al huevo dentro ~./virtualenv/..../app.egg. Entonces tuve que incluir el archivo de configuración en la instalación del paquete.
loxosceles
2

Solo un ejemplo: quiero ejecutar runio.py desde helper1.py

Ejemplo de árbol de proyecto:

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

Obtener la raíz del proyecto:

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

Construir ruta al script:

runme = os.path.join(rootdir, "tools_dir", "runio.py")
execfile(runme)
Alex Granovsky
fuente
1

Esto funcionó para mí usando un proyecto estándar de PyCharm con mi entorno virtual (venv) en el directorio raíz del proyecto.

El código siguiente no es el más bonito, pero siempre obtiene la raíz del proyecto. Devuelve la ruta completa del directorio a venv desde la VIRTUAL_ENVvariable de entorno, por ejemplo/Users/NAME/documents/PROJECT/venv

Luego divide la ruta al final /, dando una matriz con dos elementos. El primer elemento será la ruta del proyecto, p. Ej./Users/NAME/documents/PROJECT

import os

print(os.path.split(os.environ['VIRTUAL_ENV'])[0])
Gaz_Edge
fuente
3
Esto no funcionará con configuraciones como anaconda o pipenv, ya que el entorno virtual no está contenido dentro del proyecto en esos casos.
Gripp
1

Recientemente he intentado hacer algo similar y he encontrado que estas respuestas son inadecuadas para mis casos de uso (una biblioteca distribuida que necesita detectar la raíz del proyecto). Principalmente he estado luchando contra diferentes entornos y plataformas, y todavía no he encontrado algo perfectamente universal.

Código local para proyectar

He visto este ejemplo mencionado y utilizado en algunos lugares, Django, etc.

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

Por simple que sea, solo funciona cuando el archivo en el que se encuentra el fragmento es realmente parte del proyecto. No recuperamos el directorio del proyecto, sino el directorio del fragmento

De manera similar, el enfoque sys.modules se rompe cuando se llama desde fuera del punto de entrada de la aplicación, específicamente, he observado que un hilo secundario no puede determinar esto sin relación con el módulo ' principal '. He puesto explícitamente la importación dentro de una función para demostrar una importación desde un hilo secundario, moverla al nivel superior de app.py lo solucionaría.

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

La ejecución de este programa produce un error de atributo:

>>> 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__'

... de ahí una solución basada en subprocesos

Independiente de la ubicación

Usando la misma estructura de aplicación que antes pero modificando 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))

Desglosando esto: Primero queremos encontrar con precisión el ID del hilo del hilo principal. En Python3.4 +, la biblioteca de subprocesos tiene threading.main_thread(), sin embargo, no todo el mundo usa 3.4+, por lo que buscamos en todos los subprocesos buscando el subproceso principal, salvo su ID. Si el hilo principal ya ha salido, no aparecerá en la lista threading.enumerate(). Planteamos un RuntimeError()en este caso hasta que encuentre una mejor solución.

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")

A continuación, encontramos el primer marco de pila del hilo principal. Usando la función específica de cPython sys._current_frames() obtenemos un diccionario del marco de pila actual de cada hilo. Luego, utilizando inspect.getouterframes()podemos recuperar la pila completa para el hilo principal y el primer marco. current_main_frame = sys._current_frames () [main_id] base_frame = inspect.getouterframes (current_main_frame) [- 1] Finalmente, las diferencias entre las implementaciones de Windows y Linux inspect.getouterframes()deben ser manejadas. Usar el nombre de archivo limpiado os.path.abspath()y os.path.dirname()limpiar las cosas.

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))

Hasta ahora he probado esto en Python2.7 y 3.6 en Windows, así como Python3.4 en WSL

Joseph Burnitz
fuente
0

Si está trabajando con anaconda-project, puede consultar PROJECT_ROOT desde la variable de entorno -> os.getenv ('PROJECT_ROOT'). Esto solo funciona si el script se ejecuta a través de anaconda-project run.

Si no desea que anaconda-project ejecute su script, puede consultar la ruta absoluta del binario ejecutable del intérprete de Python que está usando y extraer la cadena de ruta hasta el directorio envs exclusiv. Por ejemplo: el intérprete de Python de mi conda env se encuentra en:

/ 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]

Esto solo funciona con conda-project con estructura de proyecto fija de un anaconda-project

Domsch
fuente
0

Usé el método ../ para buscar la ruta actual del proyecto.

Ejemplo: Proyecto1 - D: \ proyectos

src

Archivos de configuración

Configuration.cfg

Ruta = "../ src / ConfigurationFiles / Configuration.cfg"

Adarsh
fuente
0

En el momento de escribir este artículo, ninguna de las otras soluciones es muy autónoma. Dependen de una variable de entorno o de la posición del módulo en la estructura del paquete. La respuesta principal con la solución 'Django' es víctima de esta última al requerir una importación relativa. También tiene la desventaja de tener que modificar un módulo en el nivel superior.

Este debería ser el enfoque correcto para encontrar la ruta del directorio del paquete de nivel superior:

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')

Funciona tomando el primer componente en la cadena de puntos contenida en __name__y usándolo como una clave en la sys.modulesque devuelve el objeto de módulo del paquete de nivel superior. Su __file__atributo contiene la ruta que queremos después de recortar /__init__.pyusando os.path.dirname().

Esta solución es autónoma. Funciona en cualquier parte de cualquier módulo del paquete, incluso en el __init__.pyarchivo de nivel superior .

Pyprohly
fuente
¿Podría agregar una breve descripción sobre su solución y cómo pueden usarla como solución?
LuRsT
0

Tuve que implementar una solución personalizada porque no es tan simple como podría pensar. Mi solución se basa en la inspección de seguimiento de pila ( inspect.stack()) + sys.pathy funciona bien sin importar la ubicación del módulo de Python en el que se invoca la función ni el intérprete (intenté ejecutarlo en PyCharm, en un shell de poesía y otros ... ). Esta es la implementación completa con comentarios:

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
daveoncode
fuente
-1

Hay muchas respuestas aquí, pero no pude encontrar algo simple que cubra todos los casos, así que permítame sugerir mi solución también:

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

alonhzn
fuente