solus-packages/common/Scripts/get-py-deps.py
Thomas Staudinger c2e5aa6a72
get-py-deps: Fix flake8 errors
Signed-off-by: Thomas Staudinger <Staudi.Kaos@gmail.com>
2025-02-17 23:43:04 +01:00

217 lines
7.8 KiB
Python

#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers
#
# SPDX-License-Identifier: MPL-2.0
import argparse
import atexit
import glob
import os
import shutil
import sys
import unittest
from importlib.metadata import Distribution
from packaging.requirements import Requirement
from ruamel.yaml import YAML
from pisi.api import fetch
from pisi.package import Package
SOLUS_RECIPE_FILE = ["package.yml", "package.yaml"]
parser = argparse.ArgumentParser()
parser.add_argument("location", type=str, nargs='?', help="Location of .egg-info or .dist-info directory")
eopkg_already_exists = False
@staticmethod
def get_dependencies(dependencies: list, env=None) -> list:
sanitized = []
if dependencies is None:
return sanitized
for dep in dependencies:
req = Requirement(dep)
if not req.extras:
if req.marker:
mark = req.marker
if mark.evaluate(environment=env) is True:
sanitized.append(req.name)
continue
else:
sanitized.append(req.name)
return sanitized
def usage(msg=None, ex=1):
if msg:
print(msg, file=sys.stderr)
else:
parser.print_help()
sys.exit(ex)
def parse_recipe(path) -> tuple:
yaml = YAML()
with open(path, 'r') as file:
data = yaml.load(file)
name = data.get('name')
version = data.get('version')
release = data.get('release')
return f"{name}-{version}-{release}-1-x86_64.eopkg", name
def init_recipe_parse_path():
for i in SOLUS_RECIPE_FILE:
if os.path.exists(i):
eopkg_path, package_name = parse_recipe(i)
if not os.path.exists(eopkg_path):
fetch([package_name])
else:
global eopkg_already_exists
eopkg_already_exists = True
extract_eopkg(eopkg_path)
find_python_files("install")
def find_python_files(path):
target_dirs = [".egg-info", ".dist-info"]
for root, dirs, files in os.walk(path):
for dir_name in dirs:
for substring in target_dirs:
if substring in dir_name:
print_dependencies(os.path.join(root, dir_name))
def extract_eopkg(eopkg=str):
package = Package(eopkg)
if not os.path.exists("install"):
os.makedirs("install")
package.extract_pisi_files(".")
package.extract_dir("comar", ".")
if not os.path.exists(os.path.join(".", "install")):
os.makedirs(os.path.join(".", "install"))
package.extract_install(os.path.join(".", "install"))
if os.listdir("install") == []:
os.rmdir("install")
def init_location_path(path):
if not (os.path.exists(os.path.join(path, 'METADATA')) or os.path.exists(os.path.join(path, 'PKG-INFO'))):
usage("Unable to find PKG-INFO or METADATA files in specified directory")
print_dependencies(path)
def find_mismatched_elements(list1, list2):
normalized_list1 = {item.lower().removeprefix("python-").replace("-", "_") for item in list1}
normalized_list2 = {item.lower().replace("-", "_") for item in list2}
return normalized_list1 ^ normalized_list2
def check_against_rundeps(deps):
yaml = YAML()
with open("package.yml", "r") as file:
data = yaml.load(file)
rundeps = data.get("rundeps", [])
if rundeps:
missing_elements = find_mismatched_elements(rundeps, deps)
print("Required Deps:", deps)
print("Current Run Deps:", rundeps)
print("Mismatched dependencies:", missing_elements)
print("NOTE: The 'python-' prefix is removed and '-' '_' are equivalently compared.")
def print_dependencies(path):
dependencies = Distribution.at(path).requires
printed_deps = get_dependencies(dependencies)
if printed_deps:
check_against_rundeps(printed_deps)
if __name__ == "__main__":
args = parser.parse_args()
if args.location and not os.path.exists(args.location):
usage()
if not any(os.path.exists(path) for path in SOLUS_RECIPE_FILE) and args.location is None:
usage(msg="Expects a package.yml in current directory or pass a .egg-info or .dist-info path")
if args.location is None:
init_recipe_parse_path()
else:
init_location_path(args.location)
# Cleanup on exit
@atexit.register
def cleanuponexit():
if eopkg_already_exists is False:
matched_files = [file_path for file_path in glob.glob("*.eopkg")]
for i in matched_files:
os.unlink(i)
if os.path.exists("install"):
shutil.rmtree("install")
if os.path.exists("files.xml"):
os.unlink("files.xml")
if os.path.exists("metadata.xml"):
os.unlink("metadata.xml")
class Tests(unittest.TestCase):
def test_basic(self):
self.assertEqual(get_dependencies(['six']), ['six'])
def test_empty_list(self):
self.assertEqual(get_dependencies([]), [])
def test_excludes_version(self):
self.assertEqual(get_dependencies(['six>=6.9']), ['six'])
def test_excludes_extras(self):
list1 = ['MarkupSafe>=2.0', 'Babel>=2.7; extra == "i18n"']
self.assertEqual(get_dependencies(list1), ['MarkupSafe'])
def test_excludes_extras_no_deps(self):
self.assertEqual(get_dependencies(['Babel>=2.7; extra == "i18n"', 'pytest; extra == "testing"']), [])
def test_excludes_extras_comprehensive(self):
list2 = ['MarkupSafe>=0.9.2', 'Babel; extra == "babel"',
'lingua; extra == "lingua"', 'pytest; extra == "testing"']
self.assertEqual(get_dependencies(list2), ['MarkupSafe'])
def test_ignore_satisfied_evaluate_markers(self):
env = {'python_version': '3.11'}
list3 = ["tomli>=1.2.2; python_version < '3.11'", 'pluggy>=1.0.0']
self.assertEqual(get_dependencies(list3, env), ['pluggy'])
def test_included_unsatisified_evaluate_markers(self):
env = {'python_version': '3.11'}
list4 = ['pluggy', "tomli>=1.2.2; python_version < '3.12'"]
self.assertEqual(get_dependencies(list4, env), ['pluggy', 'tomli'])
def test_comprehensive1(self):
env = {'python_version': '3.11'}
list5 = ['pytest; extra == "testing"', 'editables>=0.3', 'packaging>=21.3',
'pathspec>=0.10.1', 'pluggy>=1.0.0', "tomli>=1.2.2; python_version < '3.12'", 'trove-classifiers']
self.assertEqual(get_dependencies(list5, env), ['editables', 'packaging', 'pathspec',
'pluggy', 'tomli', 'trove-classifiers'])
def test_comprehensive2(self):
list6 = ["brotli; implementation_name == 'cpython'", "brotlicffi; implementation_name != 'cpython'", 'certifi', 'mutagen', 'pycryptodomex', 'requests<3,>=2.32.2', 'urllib3<3,>=1.26.17', 'websockets>=12.0', "build; extra == 'build'", "hatchling; extra == 'build'", "pip; extra == 'build'", "setuptools>=71.0.2; extra == 'build'", "wheel; extra == 'build'", "curl-cffi!=0.6.*,<0.8,>=0.5.10; (os_name != 'nt' and implementation_name == 'cpython') and extra == 'curl-cffi'", "curl-cffi==0.5.10; (os_name == 'nt' and implementation_name == 'cpython') and extra == 'curl-cffi'", "pre-commit; extra == 'dev'", "yt-dlp[static-analysis]; extra == 'dev'", "yt-dlp[test]; extra == 'dev'", "py2exe>=0.12; extra == 'py2exe'", "pyinstaller>=6.7.0; extra == 'pyinstaller'", "cffi; extra == 'secretstorage'", "secretstorage; extra == 'secretstorage'", "autopep8~=2.0; extra == 'static-analysis'", "ruff~=0.5.0; extra == 'static-analysis'", "pytest~=8.1; extra == 'test'"] # noqa: E501 There is no sane way to split all of this up into short enough lines
self.assertEqual(get_dependencies(list6), ['brotli', 'certifi', 'mutagen', 'pycryptodomex',
'requests', 'urllib3', 'websockets'])