Как я могу получить текущее название «пакета»? (Setup.py) - PullRequest
4 голосов
/ 22 февраля 2020

Как мне получить текущий верхний пакет, то есть имя, определенное в setup.py?

Вот мой tree:

.
|-- README.md
|-- the_project_name_for_this_pkg
|   |-- __init__.py
|   |-- __main__.py
|   |-- _config
|   |   `-- foo.conf
|   |-- _data
|   |   `-- logging.yml
|   `-- tests
|       |-- __init__.py
|       `-- test_foo.py   <--- # executing from here
|-- requirements.txt
`-- setup.py

4 directories, 9 files

Единственное решение, которое у меня есть дошло до работы вот так:

import os
import sys


os.path.basename(sys.path[1])

Но это, очевидно, плохое решение. Другие решения, такие как __name__ в моем самом верхнем __init__.py файле и использование ast.parse для чтения в соответствующем разделе setup.py, также кажутся громоздкими.

Другие решения I ' мы пытались - позвонив им в unittest.TestCase наследуя class в моем tests python [sub] пакете - включить проверку sys.modules[__name__], inspect.getmodule & inspect.stack, а также ответы на эти вопросы:

КСТАТИ: Если вам интересно, почему я хочу имя пакета ... я могу запускать такие вещи, как:

import pkg_resources


version   = pkg_resources.require('the_project_name_for_this_pkg')[0].version
data_file = path.join(resource_filename('the_project_name_for_this_pkg', '__init__.py'),
                      '_config', 'data_file.txt')

Ответы [ 2 ]

3 голосов
/ 22 февраля 2020

Не совсем уверен, какова большая цель, но, возможно, вам может быть интересно прочитать о importlib.resources, а также importlib.metadata.

Что-то например:

import importlib.metadata
import importlib.resources

version = importlib.metadata.version('SomeProject')
data = importlib.resources.files('top_level_package.sub_package').joinpath('file.txt').read_text()

И вообще, почти невозможно (или не стоит потратить много времени) на 100% надежное определение имени проекта (SomeProject) из кода. Его проще просто жестко закодировать.

Тем не менее, вот некоторые методы и идеи для извлечения названия проекта из одного из его модулей:


Обновление :

Я полагаю, что некоторые функции, подобные следующей, должны возвращать имя установленного дистрибутива, содержащего текущий файл:

import pathlib
import importlib_metadata

def get_project_name():
    for dist in importlib_metadata.distributions():
        try:
            relative = pathlib.Path(__file__).relative_to(dist.locate_file(''))
        except ValueError:
            pass
        else:
            if relative in dist.files:
                return dist.metadata['Name']
    return None
0 голосов
/ 22 февраля 2020

Решение, над которым я работал:

from os import listdir, path
from contextlib import suppress
import ast



def get_first_setup_py(cur_dir):
    if 'setup.py' in listdir(cur_dir):
        return path.join(cur_dir, 'setup.py')
    prev_dir = cur_dir
    cur_dir = path.realpath(path.dirname(cur_dir))
    if prev_dir == cur_dir:
        raise StopIteration()
    return get_first_setup_py(cur_dir)


setup_py_file_name = get_first_setup_py(path.dirname(__file__))

Первый проход:

def get_from_setup_py(setup_file): # mostly https://stackoverflow.com/a/47463422
    import importlib.util


    spec = importlib.util.spec_from_file_location('setup', setup_file)
    setup = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(setup)
    # And now access it
    print(setup)

Эта опция сработала. Поэтому я вернулся к решению ast, на которое я ссылался в вопросе, и получил второй проход для работы:

def parse_package_name_from_setup_py(setup_py_file_name):
    with open(setup_py_file_name, 'rt') as f:
        parsed_setup_py = ast.parse(f.read(), 'setup.py')

    # Assumes you have an `if __name__ == '__main__'` block:
    main_body = next(sym for sym in parsed_setup_py.body[::-1]
                     if isinstance(sym, ast.If)).body

    setup_call = next(sym.value
                      for sym in main_body[::-1]
                      if isinstance(sym, ast.Expr)
                      and isinstance(sym.value, ast.Call)
                      and sym.value.func.id in frozenset(('setup',
                                                          'distutils.core.setup',
                                                          'setuptools.setup')))

    package_name = next(keyword
                        for keyword in setup_call.keywords
                        if keyword.arg == 'name'
                        and isinstance(keyword.value, ast.Name))

    # Return the raw string if it is one
    if isinstance(package_name.value, ast.Str):
        return package_name.value.s

    # Otherwise it's a variable defined in the `if __name__ == '__main__'` block:
    elif isinstance(package_name.value, ast.Name):
        return next(sym.value.s
                    for sym in main_body
                    if isinstance(sym, ast.Assign)
                    and isinstance(sym.value, ast.Str)
                    and any(target.id == package_name.value.id
                            for target in sym.targets)
                    )

    else:
        raise NotImplemented('Package name extraction only built for raw strings & '
                             'assigment in the same scope that setup() is called')

Третий проход (работает как для установленной, так и для разрабатываемой версии):

# Originally from https://stackoverflow.com/a/56032725;
# but made more concise and added support whence source
class App(object):
    def get_app_name(self) -> str:
        # Iterate through all installed packages and try to find one
        # that has the app's file in it
        app_def_path = inspect.getfile(self.__class__)
        with suppress(FileNotFoundError):
            return next(
                (dist.project_name
                 for dist in pkg_resources.working_set
                 if any(app_def_path == path.normpath(path.join(dist.location, r[0]))
                        for r in csv.reader(dist.get_metadata_lines('RECORD')))),
                None) or parse_package_name_from_setup_py(
                get_first_setup_py(path.dirname(__file__)))
...