Решение, над которым я работал:
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__)))