Python - pobierz ścieżkę do struktury projektu głównego

127

Mam projekt w języku Python z plikiem konfiguracyjnym w katalogu głównym projektu. Plik konfiguracyjny musi być dostępny w kilku różnych plikach w całym projekcie.

Tak to wygląda mniej więcej tak: <ROOT>/configuration.conf <ROOT>/A/a.py, <ROOT>/A/B/b.py(gdy b, a.py otworzyć plik konfiguracyjny).

Jaki jest najlepszy / najłatwiejszy sposób na uzyskanie ścieżki do katalogu głównego projektu i pliku konfiguracyjnego bez zależności od tego, w którym pliku w projekcie jestem? tj. bez używania ../../? Można założyć, że znamy nazwę katalogu głównego projektu.

Shookie
źródło
nie <ROOT>/__init__.pyistnieje?
mgilson
Albo twój plik konfiguracyjny jest modułem Pythona i możesz łatwo uzyskać do niego dostęp za pomocą instrukcji importu, albo nie jest to moduł Pythona i powinieneś umieścić go w dobrze znanej lokalizacji. Na przykład $ HOME / .my_project / my_project.conf.
John Smith Opcjonalnie
@JohnSmithOptional - To plik JSON. Muszę mieć do niego dostęp za pomocą ścieżki. Tak. Wszystkie foldery zawierają go.
Shookie
_ Można założyć, że znamy nazwę katalogu głównego projektu. _ Czy to oznacza, że ​​znasz ścieżkę do projektu? Czy to nie jest po prostu os.path.join (znana_nazwa_rootu, "konfiguracja.conf")?
tdelaney
Jeśli jest to konfiguracja użytkownika, zwykle używałbym czegoś takiego os.path.expanduser('~/.myproject/myproject.conf'). Działa na systemach Unix i Windows.
John Smith Opcjonalnie

Odpowiedzi:

158

Możesz to zrobić tak, jak robi to Django: zdefiniuj zmienną w katalogu głównym projektu z pliku znajdującego się na najwyższym poziomie projektu. Na przykład, jeśli tak wygląda struktura projektu:

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

W definitions.pymożesz zdefiniować (wymaga to import os):

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

Tak więc, znając Project Root , możesz utworzyć zmienną wskazującą na lokalizację konfiguracji (można to zdefiniować w dowolnym miejscu, ale logicznym miejscem byłoby umieszczenie jej w miejscu, w którym zdefiniowane są stałe - np. definitions.py):

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

Następnie można łatwo uzyskać dostęp do stałej (w każdym z pozostałych plików) za pomocą instrukcji import (np utils.py) from definitions import CONFIG_PATH.

jrd1
źródło
1
Czy aby dołączyć taki plik definitions.py, trzeba będzie dodać __init__.pyplik również do katalogu głównego projektu? Czy to powinno być poprawne? Właśnie zacząłem od Pythona i nie wiem, jakie są najlepsze praktyki. Dzięki.
akskap
3
@akskap: Nie, __init__.pynie będą wymagane, jako że plik jest wymagane tylko przy definiowaniu pakiety: Te __init__.pypliki są wymagane, aby Python traktować jak katalogi zawierające pakiety; ma to na celu zapobieganie przypadkowemu ukryciu prawidłowych modułów, które pojawiają się później na ścieżce wyszukiwania modułów, przez katalogi o wspólnej nazwie, takiej jak łańcuch. W najprostszym przypadku __init__.pymoże to być po prostu pusty plik, ale może również wykonać kod inicjujący dla pakietu lub ustawić __all__zmienną, co zostanie opisane później. Zobacz: docs.python.org/3/tutorial/modules.html#packages
jrd1
Jestem ciekawy, pod względem stylu, czy dodanie tych definicji do __init.py__pakietu głównego jest dopuszczalne, czy nie jest mile widziane . Oszczędziłoby to tworzenie kolejnego pliku, a także pozwoliłoby na ładniejszą składnię from root_pack import ROOT_DIR, CONFIG_PATH.
Johndt6
@ Johndt6: konwencja ma pozostać __init__.pypusta, ale to nie jest do końca prawdą (w końcu jest to konwencja). Zobacz to więcej: stackoverflow.com/questions/2361124/using-init-py
jrd1
1
@JavNoor: brak - w cytowanym przykładzie ty, os.path.abspathdzwoni ciąg, '__file__'. Przypomnij sobie, że w __file__rzeczywistości jest to atrybut importu zdefiniowany dla modułów Pythona. W tym przypadku __file__zwróci ścieżkę, z której moduł jest ładowany. Przeczytaj więcej tutaj (zobacz sekcję dotyczącą modułów): docs.python.org/3/reference/datamodel.html
jrd1,
62

Inne porady dotyczące używania pliku na najwyższym poziomie projektu. Nie jest to konieczne, jeśli używasz pathlib.Pathi parent(Python 3.4 i nowsze). Rozważ następującą strukturę katalogów, w której wszystkie pliki z wyjątkiem README.mdi utils.pyzostały pominięte.

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

W utils.pydefiniujemy następującą funkcję.

from pathlib import Path

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

W dowolnym module w projekcie możemy teraz pobrać katalog główny projektu w następujący sposób.

from src.utils import get_project_root

root = get_project_root()

Korzyści : Dowolny moduł, którego wywołania get_project_rootmożna przenosić bez zmiany zachowania programu. Dopiero po przeniesieniu modułu utils.pymusimy zaktualizować get_project_rooti importować (można to zautomatyzować za pomocą narzędzi refaktoryzacyjnych).

RikH
źródło
2
Każdy moduł znajdujący się w katalogu głównym. Wywołanie src.utils spoza katalogu głównego nie powinno działać. Czy się mylę?
aerijman
nazwa „ plik ” nie jest zdefiniowana, dlaczego?
Luk Aron
26

Wszystkie poprzednie rozwiązania wydają się być zbyt skomplikowane dla tego, czego myślę, że potrzebujesz i często nie działały dla mnie. Następujące jednowierszowe polecenie robi to, co chcesz:

import os
ROOT_DIR = os.path.abspath(os.curdir)
Martim
źródło
3
Umieść to w config.py, w katalogu głównym katalogu, ... bamn! Masz sobie singletona.
swdev
2
Ta metoda zakłada, że ​​uruchamiasz aplikację ze ścieżki, w której ona istnieje. Wielu „użytkowników” ma ikonę, którą klika na pulpicie lub może całkowicie uruchomić aplikację z innego katalogu.
DevPlayer
23

Aby uzyskać ścieżkę do modułu „root”, możesz użyć:

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

Ale co ciekawsze, jeśli masz "obiekt" konfiguracyjny w swoim najwyższym module, możesz go odczytać w następujący sposób:

app = sys.modules['__main__']
stuff = app.config.somefunc()
DevPlayer
źródło
1
Tutaj osnie jest domyślnie dostępny. Musisz zaimportować os. Więc dodanie linii import osuczyniłoby odpowiedź bardziej kompletną.
Md. Abu Nafee Ibna Zahid
5
To daje katalog zawierający skrypt, który został wykonany. Na przykład podczas uruchamiania python3 -m topmodule.submodule.scriptda /path/to/topmodule/submodulezamiast /path/to/topmodule.
danijar
14

Standardowym sposobem osiągnięcia tego byłoby użycie pkg_resourcesmodułu, który jest częścią setuptoolspakietu. setuptoolssłuży do tworzenia instalowalnego pakietu języka Python.

Możesz użyć, pkg_resourcesaby zwrócić zawartość żądanego pliku jako ciąg znaków i możesz użyć, pkg_resourcesaby uzyskać rzeczywistą ścieżkę żądanego pliku w systemie.

Powiedzmy, że masz pakiet o nazwie stackoverflow.

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

3 directories, 7 files

Teraz powiedzmy, że chcesz uzyskać dostęp do pliku Rush z modułu app.run. Użyj, pkg_resources.resouces_filenameaby uzyskać ścieżkę do Rush i pkg_resources.resource_stringzdobyć zawartość Rush; w ten sposób:

import pkg_resources

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

Wyjście:

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

Działa to dla wszystkich pakietów w twojej ścieżce Pythona. Więc jeśli chcesz wiedzieć, gdzie lxml.etreeistnieje w twoim systemie:

import pkg_resources

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

wynik:

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

Chodzi o to, że możesz użyć tej standardowej metody, aby uzyskać dostęp do plików, które są zainstalowane w twoim systemie (np. Pip install xxx lub yum -y install python-xxx) i plików, które są w module, nad którym aktualnie pracujesz.

złośnica
źródło
1
Podoba mi się twój wybór zespołu!
dylan_fan
4

Poniżej kodu Zwraca ścieżkę do katalogu głównego projektu

import sys
print(sys.path[1])
Arpan Saini
źródło
Niezła wskazówka! Zastanawiam się, dlaczego nikt oprócz mnie nie zagłosował za Twoją odpowiedzią: P
daveoncode
Dzięki Daveon Naprawdę to doceniam !!
Arpan Saini
Niestety nie jest to takie proste: P ... spójrz na moje pełne rozwiązanie: stackoverflow.com/a/62510836/267719
daveoncode
3

Próbować:

ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
złupić
źródło
1
Właśnie tego potrzebowałem. Proste rozwiązanie, działa dla mnie, ponieważ moja struktura to root-> config-> conf.py. Chciałem zdefiniować katalog główny projektu w conf.py, a root był dokładnie dwa poziomy wyżej od tego pliku.
Daniyal Arshad
2

Z tym problemem też się zmagałem, aż doszedłem do tego rozwiązania. To moim zdaniem najczystsze rozwiązanie.

W swoim setup.py dodaj „pakiety”

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

W twoim 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')
Chłopak
źródło
Korzystanie ze środowiska wirtualnego i instalowanie z python3 setup.py installnim pakietu nie wskazywało już na folder kodu źródłowego, ale na znajdujące się w nim jajko ~./virtualenv/..../app.egg. Musiałem więc dołączyć plik konfiguracyjny do instalacji pakietu.
loxosceles
2

Oto przykład: chcę uruchomić plik runio.py z poziomu helper1.py

Przykład drzewa projektu:

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

Pobierz katalog główny projektu:

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

Zbuduj ścieżkę do skryptu:

runme = os.path.join(rootdir, "tools_dir", "runio.py")
execfile(runme)
Alex Granovsky
źródło
1

Pomogło mi to przy użyciu standardowego projektu PyCharm z moim środowiskiem wirtualnym (venv) w katalogu głównym projektu.

Poniższy kod nie jest najładniejszy, ale konsekwentnie pobiera katalog główny projektu. Zwraca pełną ścieżkę do katalogu venv z plikuVIRTUAL_ENV zmiennej środowiskowej, np/Users/NAME/documents/PROJECT/venv

Następnie na końcu dzieli ścieżkę /, tworząc tablicę z dwoma elementami. Pierwszym elementem będzie ścieżka projektu np/Users/NAME/documents/PROJECT

import os

print(os.path.split(os.environ['VIRTUAL_ENV'])[0])
Gaz_Edge
źródło
3
To nie zadziała z konfiguracjami takimi jak anaconda lub pipenv, ponieważ w takich przypadkach środowisko wirtualne nie jest zawarte w projekcie.
Gripp,
1

Ostatnio próbowałem zrobić coś podobnego i stwierdziłem, że te odpowiedzi nie są odpowiednie dla moich przypadków użycia (rozproszonej biblioteki, która musi wykryć root projektu). Przede wszystkim walczyłem w różnych środowiskach i platformach i nadal nie znalazłem czegoś idealnie uniwersalnego.

Kod lokalny dla projektu

Widziałem ten przykład wspomniany i używany w kilku miejscach, Django itp.

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

Jest to proste, działa tylko wtedy, gdy plik, w którym znajduje się fragment kodu, jest faktycznie częścią projektu. Nie pobieramy katalogu projektu, ale zamiast tego katalog fragmentu

Podobnie, podejście sys.modules psuje się, gdy jest wywoływane spoza punktu wejścia aplikacji, w szczególności zauważyłem, że wątek potomny nie może tego określić bez odniesienia do modułu „ głównego ”. Jawnie umieściłem import wewnątrz funkcji, aby zademonstrować import z wątku potomnego, przeniesienie go na najwyższy poziom app.py naprawiłoby to.

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

Uruchomienie tego programu powoduje błąd atrybutu:

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

... stąd rozwiązanie oparte na wątkach

Lokalizacja niezależna

Używając tej samej struktury aplikacji co poprzednio, ale modyfikując 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))

Podsumowanie: Najpierw chcemy dokładnie znaleźć identyfikator wątku głównego wątku. Jednak w Pythonie 3.4 + biblioteka wątków nie threading.main_thread()wszyscy używają wersji 3.4+, więc przeszukujemy wszystkie wątki szukając głównego wątku, zachowując jego identyfikator. Jeśli główny wątek został już zamknięty, nie będzie wymieniony w threading.enumerate(). Podnosimy RuntimeError()w tej sprawie, dopóki nie znajdę lepszego rozwiązania.

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

Następnie znajdujemy pierwszą ramkę stosu głównego wątku. Używając funkcji specyficznej dla cPythona sys._current_frames() , otrzymujemy słownik bieżącej ramki stosu każdego wątku. Następnie wykorzystując inspect.getouterframes()możemy pobrać cały stos dla głównego wątku i pierwszej klatki. current_main_frame = sys._current_frames () [main_id] base_frame = inspect.getouterframes (current_main_frame) [- 1] Wreszcie, należy zająć się różnicami między implementacjami systemów Windows i Linux inspect.getouterframes(). Używając oczyszczonej nazwy pliku os.path.abspath()i os.path.dirname()wyczyść wszystko.

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

Do tej pory testowałem to na Pythonie2.7 i 3.6 w systemie Windows oraz Python3.4 na WSL

Joseph Burnitz
źródło
0

Jeśli pracujesz z anaconda-project, możesz wysłać zapytanie do PROJECT_ROOT ze zmiennej środowiskowej -> os.getenv ('PROJECT_ROOT'). Działa to tylko wtedy, gdy skrypt jest wykonywany poprzez uruchomienie anaconda-project.

Jeśli nie chcesz, aby Twój skrypt był uruchamiany przez anaconda-project, możesz zapytać o bezwzględną ścieżkę wykonywalnego pliku binarnego interpretera języka Python, którego używasz, i wyodrębnić ciąg ścieżki do katalogu envs exclusive. Na przykład: interpreter języka Python w moim conda env znajduje się pod adresem:

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

Działa to tylko z projektem conda ze stałą strukturą projektu anakondy

Domsch
źródło
0

Użyłem metody ../ do pobrania bieżącej ścieżki projektu.

Przykład: Project1 - D: \ projects

src

Pliki konfiguracyjne

Configuration.cfg

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

Adarsh
źródło
0

W chwili pisania tego tekstu żadne inne rozwiązania nie są bardzo samodzielne. Zależą one od zmiennej środowiskowej lub pozycji modułu w strukturze pakietu. Najlepsza odpowiedź z rozwiązaniem „Django” pada ofiarą tego ostatniego, ponieważ wymaga względnego importu. Ma również tę wadę, że trzeba modyfikować moduł na najwyższym poziomie.

Powinno to być poprawne podejście do znajdowania ścieżki katalogu pakietu najwyższego poziomu:

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

Działa poprzez pobranie pierwszego składnika z kropkowanego ciągu zawartego w nim __name__i użycie go jako klucza, w sys.modulesktórym zwraca obiekt modułu z pakietu najwyższego poziomu. Jego __file__atrybut zawiera ścieżkę, którą chcemy po przycięciu za /__init__.pypomocą os.path.dirname().

To rozwiązanie jest samowystarczalne. Działa w dowolnym miejscu w dowolnym module pakietu, w tym w __init__.pypliku najwyższego poziomu .

Pyprohly
źródło
Czy możesz dodać krótki opis swojego rozwiązania i jak mogą go użyć jako swojego rozwiązania?
LuRsT
0

Musiałem wdrożyć niestandardowe rozwiązanie, ponieważ nie jest to tak proste, jak mogłoby się wydawać. Moje rozwiązanie opiera się na inspekcji stosu śledzenia ( inspect.stack()) + sys.pathi działa dobrze bez względu na lokalizację modułu Pythona, w którym funkcja jest wywoływana, ani interpretera (próbowałem, uruchamiając go w PyCharm, w powłoce poezji i innych ... ). Oto pełna realizacja z komentarzami:

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
źródło
-1

Jest tu wiele odpowiedzi, ale nie mogłem znaleźć czegoś prostego, który obejmowałby wszystkie przypadki, więc pozwól mi zasugerować również moje rozwiązanie:

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
źródło