From 5837715e675f15ae235bbfdbfc27609e05093a07 Mon Sep 17 00:00:00 2001 From: royalvedant Date: Mon, 26 Jan 2026 17:24:14 +0530 Subject: [PATCH] feat: add pylock.toml support (PEP 751 lockfile) --- src/packagedcode/pypi.py | 68 ++++++++++++++++++++++++++++--- tests/data/pylock.toml | 34 ++++++++++++++++ tests/packagedcode/test_pylock.py | 17 ++++++++ 3 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 tests/data/pylock.toml create mode 100644 tests/packagedcode/test_pylock.py diff --git a/src/packagedcode/pypi.py b/src/packagedcode/pypi.py index b5588ed7ca..b7b9bff318 100644 --- a/src/packagedcode/pypi.py +++ b/src/packagedcode/pypi.py @@ -18,6 +18,10 @@ import sys import tempfile import zipfile + +# NOTE: adjust the location to the actual code in packagedcode/python.py +def is_pylock_toml(location): + return location.endswith("pylock.toml") from configparser import ConfigParser from fnmatch import fnmatchcase from pathlib import Path @@ -45,14 +49,66 @@ from packagedcode.utils import yield_dependencies_from_package_resource from packagedcode.utils import get_base_purl + +class PyLockPackage(models.PackageData): + datasource_id = 'pylock_toml' + type = 'python' + primary_language = 'Python' + + # tomli was added to the stdlib as tomllib in Python 3.11. # It's the same code. # Still, prefer tomli if it's installed, as on newer Python versions, it is -# compiled with mypyc and is more performant. -try: - import tomli as tomllib -except ImportError: - import tomllib + # compiled with mypyc and is more performant. + try: + import tomli as tomllib # Python 3.11+ + except ImportError: + import tomllib + +# NOTE: adjust the location to the actual code in packagedcode/python.py +def parse_pylock(location): + with open(location, "rb") as f: + data = tomllib.load(f) + return data + +def extract_pylock_packages(pylock_data): + packages = [] + + for pkg in pylock_data.get("package", []): + packages.append({ + "name": pkg.get("name"), + "version": pkg.get("version"), + "dependencies": [ + dep.get("name") for dep in pkg.get("dependencies", []) + ], + "hashes": pkg.get("hashes", []), + "source": pkg.get("source", {}), + }) + + return packages + + +from packagedcode.models import PyLockPackage + +def parse_pylock_toml(location, package_only=False): + data = parse_pylock(location) + packages = extract_pylock_packages(data) + + results = [] + for pkg in packages: + results.append( + PyLockPackage( + name=pkg["name"], + version=pkg["version"], + dependencies=pkg["dependencies"], + extra_data={ + "hashes": pkg["hashes"], + "source": pkg["source"], + } + ) + ) + return results + try: from zipfile import Path as ZipPath @@ -1214,6 +1270,8 @@ class BaseDependencyFileHandler(models.DatafileHandler): @classmethod def parse(cls, location, package_only=False): + if is_pylock_toml(location): + return parse_pylock_toml(location) file_name = fileutils.file_name(location) dependency_type = get_dparse2_supported_file_name(file_name) diff --git a/tests/data/pylock.toml b/tests/data/pylock.toml new file mode 100644 index 0000000000..cad5cfe871 --- /dev/null +++ b/tests/data/pylock.toml @@ -0,0 +1,34 @@ +package = [ + { + name = "click", + version = "8.0.4", + dependencies = [ + "setuptools", + ], + hashes = [ + "sha256:d826a4f4d2f8087796d859424c585c5c99141d6b052140a3f707011d17960d75", + ], + source = { + type = "url", + url = "https://files.pythonhosted.org/packages/source/c/click/click-8.0.4.tar.gz", + hashes = { + sha256 = "d826a4f4d2f8087796d859424c585c5c99141d6b052140a3f707011d17960d75", + }, + }, + }, + { + name = "setuptools", + version = "60.5.0", + dependencies = [], + hashes = [ + "sha256:8b030b7e28b8b42c4b8e7e1b5b8c9d0d3a5a7f9a1f3a9e3a3c2a8f8a1a1a1a1a", + ], + source = { + type = "url", + url = "https://files.pythonhosted.org/packages/source/s/setuptools/setuptools-60.5.0.tar.gz", + hashes = { + sha256 = "8b030b7e28b8b42c4b8e7e1b5b8c9d0d3a5a7f9a1f3a9e3a3c2a8f8a1a1a1a1a", + }, + }, + }, +] diff --git a/tests/packagedcode/test_pylock.py b/tests/packagedcode/test_pylock.py new file mode 100644 index 0000000000..399cd46a56 --- /dev/null +++ b/tests/packagedcode/test_pylock.py @@ -0,0 +1,17 @@ +from os.path import join, dirname, abspath + +from packagedcode.pypi import is_pylock_toml +from packagedcode.pypi import parse_pylock + +TEST_DATA_DIR = join(abspath(dirname(__file__)), 'data') + +def test_is_pylock_toml(): + assert is_pylock_toml("pylock.toml") + +def test_parse_pylock(): + location = join(TEST_DATA_DIR, "pylock.toml") + data = parse_pylock(location) + assert "package" in data + assert len(data["package"]) == 2 + assert data["package"][0]["name"] == "click" + assert data["package"][1]["name"] == "setuptools" \ No newline at end of file