diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d115cda4b80..7f22cf7850e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,14 @@ Changelog Next release -------------- +- Add support for the Python UV package manager. Two new package data + handlers parse ``pyproject.toml`` files containing a ``[tool.uv]`` table + and ``uv.lock`` lockfiles, including PEP 735 ``[dependency-groups]``, + and the package assembly walks both files together so that the project + metadata and the resolved transitive dependencies are reported as a + single Python package. + https://github.com/aboutcode-org/scancode-toolkit/issues/4501 + v3.5.0 - 2026-01-15 ------------------- diff --git a/src/packagedcode/__init__.py b/src/packagedcode/__init__.py index d3c48b6e259..fc1e490eeff 100644 --- a/src/packagedcode/__init__.py +++ b/src/packagedcode/__init__.py @@ -174,6 +174,8 @@ pypi.PyprojectTomlHandler, pypi.PoetryPyprojectTomlHandler, pypi.PoetryLockHandler, + pypi.UvPyprojectTomlHandler, + pypi.UvLockHandler, pypi.PythonEditableInstallationPkgInfoFile, pypi.PythonEggPkgInfoFile, pypi.PythonInstalledWheelMetadataFile, diff --git a/src/packagedcode/pypi.py b/src/packagedcode/pypi.py index b5588ed7ca9..1a36a162896 100644 --- a/src/packagedcode/pypi.py +++ b/src/packagedcode/pypi.py @@ -467,6 +467,7 @@ def is_datafile(cls, location, filetypes=tuple()): return ( super().is_datafile(location, filetypes=filetypes) and not is_poetry_pyproject_toml(location) + and not is_uv_pyproject_toml(location) ) @classmethod @@ -832,6 +833,288 @@ def parse(cls, location, package_only=False): yield models.PackageData.from_data(package_data, package_only) +def is_uv_pyproject_toml(location): + """ + Return True if the pyproject.toml file at ``location`` is for a UV + project (it contains a ``[tool.uv]`` table). + """ + with open(location, 'r') as fp: + if "[tool.uv]" in fp.read(): + return True + return False + + +def get_dependency_group_dependencies(groups): + """ + Return a list of DependentPackage parsed from a PEP 735 ``[dependency-groups]`` + mapping as found in a pyproject.toml file. ``include-group`` references are + skipped: their resolved members are emitted by their own group entry. + """ + dependencies = [] + for group_name, group_items in (groups or {}).items(): + requires = [] + for item in group_items: + # entries are either a requirement string or a mapping such as + # ``{include-group = "tests"}`` which we skip as a forward reference + if isinstance(item, str): + requires.append(item) + dependencies.extend( + get_requires_dependencies( + requires=requires, + default_scope=group_name, + is_optional=True, + is_runtime=False, + ) + ) + return dependencies + + +class BaseUvPythonLayout(BaseExtractedPythonLayout): + """ + Base class for UV-managed Python projects (``pyproject.toml`` paired with + a ``uv.lock`` lockfile). + """ + + @classmethod + def assemble(cls, package_data, resource, codebase, package_adder): + if codebase.has_single_resource: + yield from models.DatafileHandler.assemble(package_data, resource, codebase, package_adder) + return + + package_resource = None + if resource.name == 'pyproject.toml': + package_resource = resource + elif resource.name == 'uv.lock': + if resource.has_parent(): + siblings = resource.siblings(codebase) + pyprojects = [r for r in siblings if r.name == 'pyproject.toml'] + if pyprojects: + package_resource = pyprojects[0] + + if not package_resource: + yield from yield_dependencies_from_package_resource(resource) + return + + assert len(package_resource.package_data) == 1, f'Invalid pyproject.toml for {package_resource.path}' + pkg_data = package_resource.package_data[0] + pkg_data = models.PackageData.from_dict(pkg_data) + + package_uid = None + if pkg_data.purl: + package = models.Package.from_package_data( + package_data=pkg_data, + datafile_path=package_resource.path, + ) + package_uid = package.package_uid + package.populate_license_fields() + yield package + + root = package_resource.parent(codebase) + if root: + for pypi_res in cls.walk_pypi(resource=root, codebase=codebase): + if package_uid and package_uid not in pypi_res.for_packages: + package_adder(package_uid, pypi_res, codebase) + yield pypi_res + + yield package_resource + + yield from yield_dependencies_from_package_data(pkg_data, package_resource.path, package_uid) + + yield package_resource + + for lock_file in package_resource.siblings(codebase): + if lock_file.name == 'uv.lock': + yield from yield_dependencies_from_package_resource(lock_file, package_uid) + + if package_uid and package_uid not in lock_file.for_packages: + package_adder(package_uid, lock_file, codebase) + yield lock_file + + +class UvPyprojectTomlHandler(BaseUvPythonLayout): + datasource_id = 'pypi_uv_pyproject_toml' + path_patterns = ('*pyproject.toml',) + default_package_type = 'pypi' + default_primary_language = 'Python' + description = 'Python UV pyproject.toml' + documentation_url = 'https://docs.astral.sh/uv/concepts/projects/' + + @classmethod + def is_datafile(cls, location, filetypes=tuple()): + return ( + super().is_datafile(location, filetypes=filetypes) + and is_uv_pyproject_toml(location) + ) + + @classmethod + def parse(cls, location, package_only=False): + with open(location, "rb") as fp: + toml_data = tomllib.load(fp) + + project_data = toml_data.get("project") + if not project_data: + return + + name = project_data.get('name') + version = project_data.get('version') + description = project_data.get('description') or '' + description = description.strip() + + urls, extra_data = get_urls(metainfo=project_data, name=name, version=version) + + extracted_license_statement, license_file = get_declared_license(project_data) + if license_file: + extra_data['license_file'] = license_file + + requires_python = project_data.get('requires-python') + if requires_python: + extra_data['python_requires'] = requires_python + + dependencies = [] + dependencies.extend( + get_requires_dependencies(requires=project_data.get("dependencies", [])) + ) + + for dep_type, deps in project_data.get("optional-dependencies", {}).items(): + dependencies.extend( + get_requires_dependencies( + requires=deps, + default_scope=dep_type, + is_optional=True, + ) + ) + + dependencies.extend( + get_dependency_group_dependencies(toml_data.get("dependency-groups", {})) + ) + + package_data = dict( + datasource_id=cls.datasource_id, + type=cls.default_package_type, + primary_language='Python', + name=name, + version=version, + extracted_license_statement=extracted_license_statement, + description=description, + keywords=get_keywords(project_data), + parties=get_pyproject_toml_parties(project_data), + dependencies=dependencies, + extra_data=extra_data, + **urls, + ) + yield models.PackageData.from_data(package_data, package_only) + + +class UvLockHandler(BaseUvPythonLayout): + datasource_id = 'pypi_uv_lock' + path_patterns = ('*uv.lock',) + default_package_type = 'pypi' + default_primary_language = 'Python' + description = 'Python UV lockfile' + documentation_url = 'https://docs.astral.sh/uv/concepts/projects/sync/#the-uvlock-file' + + @classmethod + def parse(cls, location, package_only=False): + with open(location, "rb") as fp: + toml_data = tomllib.load(fp) + + packages = toml_data.get('package') + if not packages: + return + + dependencies = [] + for package in packages: + source = package.get('source') or {} + # skip the editable root project entry: the local pyproject.toml is + # parsed independently and the resolved transitive dependencies are + # surfaced as their own ``[[package]]`` entries. + if 'editable' in source or 'virtual' in source: + continue + + name = package.get('name') + version = package.get('version') + if not name: + continue + + dependencies_for_resolved = [] + for dep in (package.get('dependencies') or []): + dep_name = dep.get('name') + if not dep_name: + continue + dep_purl = PackageURL(type=cls.default_package_type, name=dep_name) + dependencies_for_resolved.append( + models.DependentPackage( + purl=dep_purl.to_string(), + extracted_requirement=dep.get('marker'), + scope='dependencies', + is_runtime=True, + is_optional=False, + is_direct=True, + is_pinned=False, + ).to_dict() + ) + + sha256 = None + download_url = None + sdist = package.get('sdist') + if isinstance(sdist, dict): + download_url = sdist.get('url') + hash_value = sdist.get('hash') or '' + if hash_value.startswith('sha256:'): + sha256 = hash_value[len('sha256:'):] + + urls = get_pypi_urls(name, version) + if download_url: + # prefer the exact sdist URL recorded in the lock file + urls['repository_download_url'] = download_url + + resolved_package_data = dict( + datasource_id=cls.datasource_id, + type=cls.default_package_type, + primary_language='Python', + name=name, + version=version, + sha256=sha256, + is_virtual=True, + dependencies=dependencies_for_resolved, + **urls, + ) + resolved_package = models.PackageData.from_data(resolved_package_data, package_only) + + dependencies.append( + models.DependentPackage( + purl=resolved_package.purl, + extracted_requirement=None, + scope=None, + is_runtime=True, + is_optional=False, + is_direct=False, + is_pinned=True, + resolved_package=resolved_package.to_dict(), + ).to_dict() + ) + + extra_data = {} + requires_python = toml_data.get('requires-python') + if requires_python: + extra_data['python_requires'] = requires_python + lock_version = toml_data.get('version') + if lock_version is not None: + extra_data['lock_version'] = lock_version + revision = toml_data.get('revision') + if revision is not None: + extra_data['revision'] = revision + + package_data = dict( + datasource_id=cls.datasource_id, + type=cls.default_package_type, + primary_language='Python', + extra_data=extra_data, + dependencies=dependencies, + ) + yield models.PackageData.from_data(package_data, package_only) + + class PipInspectDeplockHandler(models.DatafileHandler): datasource_id = 'pypi_inspect_deplock' path_patterns = ('*pip-inspect.deplock',) diff --git a/tests/packagedcode/data/plugin/plugins_list_linux.txt b/tests/packagedcode/data/plugin/plugins_list_linux.txt index eb4763d6c7e..34078bc580b 100755 --- a/tests/packagedcode/data/plugin/plugins_list_linux.txt +++ b/tests/packagedcode/data/plugin/plugins_list_linux.txt @@ -790,6 +790,20 @@ Package type: pypi description: Python setup.py path_patterns: '*setup.py' -------------------------------------------- +Package type: pypi + datasource_id: pypi_uv_lock + documentation URL: https://docs.astral.sh/uv/concepts/projects/sync/#the-uvlock-file + primary language: Python + description: Python UV lockfile + path_patterns: '*uv.lock' +-------------------------------------------- +Package type: pypi + datasource_id: pypi_uv_pyproject_toml + documentation URL: https://docs.astral.sh/uv/concepts/projects/ + primary language: Python + description: Python UV pyproject.toml + path_patterns: '*pyproject.toml' +-------------------------------------------- Package type: pypi datasource_id: pypi_wheel documentation URL: https://peps.python.org/pep-0427/ diff --git a/tests/packagedcode/data/pypi/uv/attrs-package-assembly-expected.json b/tests/packagedcode/data/pypi/uv/attrs-package-assembly-expected.json new file mode 100644 index 00000000000..0234f84b2cf --- /dev/null +++ b/tests/packagedcode/data/pypi/uv/attrs-package-assembly-expected.json @@ -0,0 +1,1139 @@ +{ + "packages": [ + { + "type": "pypi", + "namespace": null, + "name": "attrs", + "version": null, + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": "Classes Without Boilerplate", + "release_date": null, + "parties": [ + { + "type": "person", + "role": "author", + "name": "Hynek Schlawack", + "email": "hs@ox.cx", + "url": null + } + ], + "keywords": [ + "class", + "attribute", + "boilerplate", + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Typing :: Typed" + ], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": "https://github.com/python-attrs/attrs", + "copyright": null, + "holder": null, + "declared_license_expression": "mit", + "declared_license_expression_spdx": "MIT", + "license_detections": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "matches": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "from_file": "attrs/pyproject.toml", + "start_line": 1, + "end_line": 1, + "matcher": "1-spdx-id", + "score": 100.0, + "matched_length": 1, + "match_coverage": 100.0, + "rule_relevance": 100, + "rule_identifier": "spdx-license-identifier-mit-5da48780aba670b0860c46d899ed42a0f243ff06", + "rule_url": null, + "matched_text": "MIT" + } + ], + "identifier": "mit-a822f434-d61f-f2b1-c792-8b8cb9e7b9bf" + } + ], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": "license: MIT\n", + "notice_text": null, + "source_packages": [], + "is_private": false, + "is_virtual": false, + "extra_data": { + "Documentation": "https://www.attrs.org/", + "Changelog": "https://www.attrs.org/en/stable/changelog.html", + "Funding": "https://github.com/sponsors/hynek", + "python_requires": ">=3.9" + }, + "repository_homepage_url": "https://pypi.org/project/attrs", + "repository_download_url": null, + "api_data_url": "https://pypi.org/pypi/attrs/json", + "package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_paths": [ + "attrs/pyproject.toml" + ], + "datasource_ids": [ + "pypi_uv_pyproject_toml" + ], + "purl": "pkg:pypi/attrs" + } + ], + "dependencies": [ + { + "purl": "pkg:pypi/cloudpickle", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {}, + "dependency_uid": "pkg:pypi/cloudpickle?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/pyproject.toml", + "datasource_id": "pypi_uv_pyproject_toml" + }, + { + "purl": "pkg:pypi/hypothesis", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {}, + "dependency_uid": "pkg:pypi/hypothesis?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/pyproject.toml", + "datasource_id": "pypi_uv_pyproject_toml" + }, + { + "purl": "pkg:pypi/pympler", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {}, + "dependency_uid": "pkg:pypi/pympler?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/pyproject.toml", + "datasource_id": "pypi_uv_pyproject_toml" + }, + { + "purl": "pkg:pypi/pytest", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {}, + "dependency_uid": "pkg:pypi/pytest?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/pyproject.toml", + "datasource_id": "pypi_uv_pyproject_toml" + }, + { + "purl": "pkg:pypi/pytest-xdist", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {}, + "dependency_uid": "pkg:pypi/pytest-xdist?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/pyproject.toml", + "datasource_id": "pypi_uv_pyproject_toml" + }, + { + "purl": "pkg:pypi/cogapp", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {}, + "dependency_uid": "pkg:pypi/cogapp?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/pyproject.toml", + "datasource_id": "pypi_uv_pyproject_toml" + }, + { + "purl": "pkg:pypi/furo", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {}, + "dependency_uid": "pkg:pypi/furo?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/pyproject.toml", + "datasource_id": "pypi_uv_pyproject_toml" + }, + { + "purl": "pkg:pypi/myst-parser", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {}, + "dependency_uid": "pkg:pypi/myst-parser?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/pyproject.toml", + "datasource_id": "pypi_uv_pyproject_toml" + }, + { + "purl": "pkg:pypi/sphinx", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {}, + "dependency_uid": "pkg:pypi/sphinx?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/pyproject.toml", + "datasource_id": "pypi_uv_pyproject_toml" + }, + { + "purl": "pkg:pypi/ruff", + "extracted_requirement": null, + "scope": "dev", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {}, + "dependency_uid": "pkg:pypi/ruff?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/pyproject.toml", + "datasource_id": "pypi_uv_pyproject_toml" + }, + { + "purl": "pkg:pypi/alabaster@0.7.16", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "alabaster", + "version": "0.7.16", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/alabaster", + "repository_download_url": "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", + "api_data_url": "https://pypi.org/pypi/alabaster/0.7.16/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/alabaster@0.7.16" + }, + "extra_data": {}, + "dependency_uid": "pkg:pypi/alabaster@0.7.16?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/uv.lock", + "datasource_id": "pypi_uv_lock" + }, + { + "purl": "pkg:pypi/beautifulsoup4@4.14.3", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "beautifulsoup4", + "version": "4.14.3", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [ + { + "purl": "pkg:pypi/soupsieve", + "extracted_requirement": null, + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/typing-extensions", + "extracted_requirement": null, + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + } + ], + "repository_homepage_url": "https://pypi.org/project/beautifulsoup4", + "repository_download_url": "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", + "api_data_url": "https://pypi.org/pypi/beautifulsoup4/4.14.3/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/beautifulsoup4@4.14.3" + }, + "extra_data": {}, + "dependency_uid": "pkg:pypi/beautifulsoup4@4.14.3?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/uv.lock", + "datasource_id": "pypi_uv_lock" + }, + { + "purl": "pkg:pypi/certifi@2026.2.25", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "certifi", + "version": "2026.2.25", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/certifi", + "repository_download_url": "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", + "api_data_url": "https://pypi.org/pypi/certifi/2026.2.25/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/certifi@2026.2.25" + }, + "extra_data": {}, + "dependency_uid": "pkg:pypi/certifi@2026.2.25?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/uv.lock", + "datasource_id": "pypi_uv_lock" + }, + { + "purl": "pkg:pypi/soupsieve@2.6", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "soupsieve", + "version": "2.6", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/soupsieve", + "repository_download_url": "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", + "api_data_url": "https://pypi.org/pypi/soupsieve/2.6/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/soupsieve@2.6" + }, + "extra_data": {}, + "dependency_uid": "pkg:pypi/soupsieve@2.6?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/uv.lock", + "datasource_id": "pypi_uv_lock" + }, + { + "purl": "pkg:pypi/typing-extensions@4.12.2", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "typing-extensions", + "version": "4.12.2", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/typing-extensions", + "repository_download_url": "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", + "api_data_url": "https://pypi.org/pypi/typing-extensions/4.12.2/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/typing-extensions@4.12.2" + }, + "extra_data": {}, + "dependency_uid": "pkg:pypi/typing-extensions@4.12.2?uuid=fixed-uid-done-for-testing-5642512d1758", + "for_package_uid": "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758", + "datafile_path": "attrs/uv.lock", + "datasource_id": "pypi_uv_lock" + } + ], + "files": [ + { + "path": "attrs", + "type": "directory", + "package_data": [], + "for_packages": [], + "scan_errors": [] + }, + { + "path": "attrs/pyproject.toml", + "type": "file", + "package_data": [ + { + "type": "pypi", + "namespace": null, + "name": "attrs", + "version": null, + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": "Classes Without Boilerplate", + "release_date": null, + "parties": [ + { + "type": "person", + "role": "author", + "name": "Hynek Schlawack", + "email": "hs@ox.cx", + "url": null + } + ], + "keywords": [ + "class", + "attribute", + "boilerplate", + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Typing :: Typed" + ], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": "https://github.com/python-attrs/attrs", + "copyright": null, + "holder": null, + "declared_license_expression": "mit", + "declared_license_expression_spdx": "MIT", + "license_detections": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "matches": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "from_file": "attrs/pyproject.toml", + "start_line": 1, + "end_line": 1, + "matcher": "1-spdx-id", + "score": 100.0, + "matched_length": 1, + "match_coverage": 100.0, + "rule_relevance": 100, + "rule_identifier": "spdx-license-identifier-mit-5da48780aba670b0860c46d899ed42a0f243ff06", + "rule_url": null, + "matched_text": "MIT" + } + ], + "identifier": "mit-a822f434-d61f-f2b1-c792-8b8cb9e7b9bf" + } + ], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": "license: MIT\n", + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": false, + "extra_data": { + "Documentation": "https://www.attrs.org/", + "Changelog": "https://www.attrs.org/en/stable/changelog.html", + "Funding": "https://github.com/sponsors/hynek", + "python_requires": ">=3.9" + }, + "dependencies": [ + { + "purl": "pkg:pypi/cloudpickle", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/hypothesis", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/pympler", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/pytest", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/pytest-xdist", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/cogapp", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/furo", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/myst-parser", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/sphinx", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/ruff", + "extracted_requirement": null, + "scope": "dev", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + } + ], + "repository_homepage_url": "https://pypi.org/project/attrs", + "repository_download_url": null, + "api_data_url": "https://pypi.org/pypi/attrs/json", + "datasource_id": "pypi_uv_pyproject_toml", + "purl": "pkg:pypi/attrs" + } + ], + "for_packages": [ + "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758" + ], + "scan_errors": [] + }, + { + "path": "attrs/uv.lock", + "type": "file", + "package_data": [ + { + "type": "pypi", + "namespace": null, + "name": null, + "version": null, + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": false, + "extra_data": { + "python_requires": ">=3.9", + "lock_version": 1, + "revision": 3 + }, + "dependencies": [ + { + "purl": "pkg:pypi/alabaster@0.7.16", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "alabaster", + "version": "0.7.16", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/alabaster", + "repository_download_url": "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", + "api_data_url": "https://pypi.org/pypi/alabaster/0.7.16/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/alabaster@0.7.16" + }, + "extra_data": {} + }, + { + "purl": "pkg:pypi/beautifulsoup4@4.14.3", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "beautifulsoup4", + "version": "4.14.3", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [ + { + "purl": "pkg:pypi/soupsieve", + "extracted_requirement": null, + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/typing-extensions", + "extracted_requirement": null, + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + } + ], + "repository_homepage_url": "https://pypi.org/project/beautifulsoup4", + "repository_download_url": "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", + "api_data_url": "https://pypi.org/pypi/beautifulsoup4/4.14.3/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/beautifulsoup4@4.14.3" + }, + "extra_data": {} + }, + { + "purl": "pkg:pypi/certifi@2026.2.25", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "certifi", + "version": "2026.2.25", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/certifi", + "repository_download_url": "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", + "api_data_url": "https://pypi.org/pypi/certifi/2026.2.25/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/certifi@2026.2.25" + }, + "extra_data": {} + }, + { + "purl": "pkg:pypi/soupsieve@2.6", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "soupsieve", + "version": "2.6", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/soupsieve", + "repository_download_url": "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", + "api_data_url": "https://pypi.org/pypi/soupsieve/2.6/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/soupsieve@2.6" + }, + "extra_data": {} + }, + { + "purl": "pkg:pypi/typing-extensions@4.12.2", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "typing-extensions", + "version": "4.12.2", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/typing-extensions", + "repository_download_url": "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", + "api_data_url": "https://pypi.org/pypi/typing-extensions/4.12.2/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/typing-extensions@4.12.2" + }, + "extra_data": {} + } + ], + "repository_homepage_url": null, + "repository_download_url": null, + "api_data_url": null, + "datasource_id": "pypi_uv_lock", + "purl": null + } + ], + "for_packages": [ + "pkg:pypi/attrs?uuid=fixed-uid-done-for-testing-5642512d1758" + ], + "scan_errors": [] + } + ] +} \ No newline at end of file diff --git a/tests/packagedcode/data/pypi/uv/attrs-pyproject.toml-expected.json b/tests/packagedcode/data/pypi/uv/attrs-pyproject.toml-expected.json new file mode 100644 index 00000000000..97d084f04a7 --- /dev/null +++ b/tests/packagedcode/data/pypi/uv/attrs-pyproject.toml-expected.json @@ -0,0 +1,205 @@ +[ + { + "type": "pypi", + "namespace": null, + "name": "attrs", + "version": null, + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": "Classes Without Boilerplate", + "release_date": null, + "parties": [ + { + "type": "person", + "role": "author", + "name": "Hynek Schlawack", + "email": "hs@ox.cx", + "url": null + } + ], + "keywords": [ + "class", + "attribute", + "boilerplate", + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Typing :: Typed" + ], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": "https://github.com/python-attrs/attrs", + "copyright": null, + "holder": null, + "declared_license_expression": "mit", + "declared_license_expression_spdx": "MIT", + "license_detections": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "matches": [ + { + "license_expression": "mit", + "license_expression_spdx": "MIT", + "from_file": null, + "start_line": 1, + "end_line": 1, + "matcher": "1-spdx-id", + "score": 100.0, + "matched_length": 1, + "match_coverage": 100.0, + "rule_relevance": 100, + "rule_identifier": "spdx-license-identifier-mit-5da48780aba670b0860c46d899ed42a0f243ff06", + "rule_url": null, + "matched_text": "MIT" + } + ], + "identifier": "mit-a822f434-d61f-f2b1-c792-8b8cb9e7b9bf" + } + ], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": "license: MIT\n", + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": false, + "extra_data": { + "Documentation": "https://www.attrs.org/", + "Changelog": "https://www.attrs.org/en/stable/changelog.html", + "Funding": "https://github.com/sponsors/hynek", + "python_requires": ">=3.9" + }, + "dependencies": [ + { + "purl": "pkg:pypi/cloudpickle", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/hypothesis", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/pympler", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/pytest", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/pytest-xdist", + "extracted_requirement": null, + "scope": "tests", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/cogapp", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/furo", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/myst-parser", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/sphinx", + "extracted_requirement": null, + "scope": "docs", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/ruff", + "extracted_requirement": null, + "scope": "dev", + "is_runtime": false, + "is_optional": true, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + } + ], + "repository_homepage_url": "https://pypi.org/project/attrs", + "repository_download_url": null, + "api_data_url": "https://pypi.org/pypi/attrs/json", + "datasource_id": "pypi_uv_pyproject_toml", + "purl": "pkg:pypi/attrs" + } +] \ No newline at end of file diff --git a/tests/packagedcode/data/pypi/uv/attrs-uv.lock-expected.json b/tests/packagedcode/data/pypi/uv/attrs-uv.lock-expected.json new file mode 100644 index 00000000000..40e956d7edc --- /dev/null +++ b/tests/packagedcode/data/pypi/uv/attrs-uv.lock-expected.json @@ -0,0 +1,344 @@ +[ + { + "type": "pypi", + "namespace": null, + "name": null, + "version": null, + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": null, + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": false, + "extra_data": { + "python_requires": ">=3.9", + "lock_version": 1, + "revision": 3 + }, + "dependencies": [ + { + "purl": "pkg:pypi/alabaster@0.7.16", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "alabaster", + "version": "0.7.16", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/alabaster", + "repository_download_url": "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", + "api_data_url": "https://pypi.org/pypi/alabaster/0.7.16/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/alabaster@0.7.16" + }, + "extra_data": {} + }, + { + "purl": "pkg:pypi/beautifulsoup4@4.14.3", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "beautifulsoup4", + "version": "4.14.3", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [ + { + "purl": "pkg:pypi/soupsieve", + "extracted_requirement": null, + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + }, + { + "purl": "pkg:pypi/typing-extensions", + "extracted_requirement": null, + "scope": "dependencies", + "is_runtime": true, + "is_optional": false, + "is_pinned": false, + "is_direct": true, + "resolved_package": {}, + "extra_data": {} + } + ], + "repository_homepage_url": "https://pypi.org/project/beautifulsoup4", + "repository_download_url": "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", + "api_data_url": "https://pypi.org/pypi/beautifulsoup4/4.14.3/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/beautifulsoup4@4.14.3" + }, + "extra_data": {} + }, + { + "purl": "pkg:pypi/certifi@2026.2.25", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "certifi", + "version": "2026.2.25", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/certifi", + "repository_download_url": "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", + "api_data_url": "https://pypi.org/pypi/certifi/2026.2.25/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/certifi@2026.2.25" + }, + "extra_data": {} + }, + { + "purl": "pkg:pypi/soupsieve@2.6", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "soupsieve", + "version": "2.6", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/soupsieve", + "repository_download_url": "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", + "api_data_url": "https://pypi.org/pypi/soupsieve/2.6/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/soupsieve@2.6" + }, + "extra_data": {} + }, + { + "purl": "pkg:pypi/typing-extensions@4.12.2", + "extracted_requirement": null, + "scope": null, + "is_runtime": true, + "is_optional": false, + "is_pinned": true, + "is_direct": false, + "resolved_package": { + "type": "pypi", + "namespace": null, + "name": "typing-extensions", + "version": "4.12.2", + "qualifiers": {}, + "subpath": null, + "primary_language": "Python", + "description": null, + "release_date": null, + "parties": [], + "keywords": [], + "homepage_url": null, + "download_url": null, + "size": null, + "sha1": null, + "md5": null, + "sha256": "1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", + "sha512": null, + "bug_tracking_url": null, + "code_view_url": null, + "vcs_url": null, + "copyright": null, + "holder": null, + "declared_license_expression": null, + "declared_license_expression_spdx": null, + "license_detections": [], + "other_license_expression": null, + "other_license_expression_spdx": null, + "other_license_detections": [], + "extracted_license_statement": null, + "notice_text": null, + "source_packages": [], + "file_references": [], + "is_private": false, + "is_virtual": true, + "extra_data": {}, + "dependencies": [], + "repository_homepage_url": "https://pypi.org/project/typing-extensions", + "repository_download_url": "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", + "api_data_url": "https://pypi.org/pypi/typing-extensions/4.12.2/json", + "datasource_id": "pypi_uv_lock", + "purl": "pkg:pypi/typing-extensions@4.12.2" + }, + "extra_data": {} + } + ], + "repository_homepage_url": null, + "repository_download_url": null, + "api_data_url": null, + "datasource_id": "pypi_uv_lock", + "purl": null + } +] \ No newline at end of file diff --git a/tests/packagedcode/data/pypi/uv/attrs/pyproject.toml b/tests/packagedcode/data/pypi/uv/attrs/pyproject.toml new file mode 100644 index 00000000000..94379f2ff47 --- /dev/null +++ b/tests/packagedcode/data/pypi/uv/attrs/pyproject.toml @@ -0,0 +1,57 @@ +# Trimmed copy of the pyproject.toml from +# https://github.com/python-attrs/attrs (release 26.1.0) for testing +# UV package manager support. Original source is MIT-licensed. +# SPDX-License-Identifier: MIT + +[build-system] +requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme>=23.2.0"] +build-backend = "hatchling.build" + + +[project] +name = "attrs" +authors = [{ name = "Hynek Schlawack", email = "hs@ox.cx" }] +license = "MIT" +license-files = ["LICENSE"] +requires-python = ">=3.9" +description = "Classes Without Boilerplate" +keywords = ["class", "attribute", "boilerplate"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Typing :: Typed", +] +dependencies = [] +dynamic = ["version", "readme"] + +[project.urls] +Documentation = "https://www.attrs.org/" +Changelog = "https://www.attrs.org/en/stable/changelog.html" +GitHub = "https://github.com/python-attrs/attrs" +Funding = "https://github.com/sponsors/hynek" + + +[dependency-groups] +tests = [ + 'cloudpickle; platform_python_implementation == "CPython"', + "hypothesis", + "pympler", + "pytest", + "pytest-xdist[psutil]", +] +docs = [ + "cogapp", + "furo", + "myst-parser", + "sphinx", +] +dev = [{ include-group = "tests" }, "ruff"] + + +[tool.uv] +package = true diff --git a/tests/packagedcode/data/pypi/uv/attrs/uv.lock b/tests/packagedcode/data/pypi/uv/attrs/uv.lock new file mode 100644 index 00000000000..37d53d2ccf2 --- /dev/null +++ b/tests/packagedcode/data/pypi/uv/attrs/uv.lock @@ -0,0 +1,96 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "alabaster" +version = "0.7.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 }, +] + +[[package]] +name = "attrs" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "hypothesis" }, + { name = "pympler" }, + { name = "pytest" }, + { name = "pytest-xdist", extra = ["psutil"] }, + { name = "ruff" }, +] +tests = [ + { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "hypothesis" }, + { name = "pympler" }, + { name = "pytest" }, + { name = "pytest-xdist", extra = ["psutil"] }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "hypothesis" }, + { name = "pympler" }, + { name = "pytest" }, + { name = "pytest-xdist", extras = ["psutil"] }, + { name = "ruff" }, +] +tests = [ + { name = "cloudpickle", marker = "platform_python_implementation == 'CPython'" }, + { name = "hypothesis" }, + { name = "pympler" }, + { name = "pytest" }, + { name = "pytest-xdist", extras = ["psutil"] }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721 }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684 }, +] + +[[package]] +name = "soupsieve" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] diff --git a/tests/packagedcode/test_pypi.py b/tests/packagedcode/test_pypi.py index 3dcfa7d4268..934ec8565c5 100644 --- a/tests/packagedcode/test_pypi.py +++ b/tests/packagedcode/test_pypi.py @@ -405,6 +405,57 @@ def test_parse_pyproject_toml_poetry_univers(self): self.check_packages_data(package, expected_loc, regen=REGEN_TEST_FIXTURES) +class TestUvHandler(PackageTester): + # Test fixtures derived from python-attrs/attrs (release 26.1.0): + # https://github.com/python-attrs/attrs (MIT-licensed). + + test_data_dir = os.path.join(os.path.dirname(__file__), 'data') + + def test_is_pyproject_toml_uv(self): + test_file = self.get_test_loc('pypi/uv/attrs/pyproject.toml') + assert pypi.UvPyprojectTomlHandler.is_datafile(test_file) + + def test_uv_pyproject_toml_excluded_from_standard_pyproject_handler(self): + test_file = self.get_test_loc('pypi/uv/attrs/pyproject.toml') + assert not pypi.PyprojectTomlHandler.is_datafile(test_file) + + def test_parse_pyproject_toml_uv_attrs(self): + test_file = self.get_test_loc('pypi/uv/attrs/pyproject.toml') + package = pypi.UvPyprojectTomlHandler.parse(test_file) + expected_loc = self.get_test_loc( + 'pypi/uv/attrs-pyproject.toml-expected.json', + must_exist=False, + ) + self.check_packages_data( + package, expected_loc, must_exist=False, regen=REGEN_TEST_FIXTURES, + ) + + def test_is_uv_lock(self): + test_file = self.get_test_loc('pypi/uv/attrs/uv.lock') + assert pypi.UvLockHandler.is_datafile(test_file) + + def test_parse_uv_lock_attrs(self): + test_file = self.get_test_loc('pypi/uv/attrs/uv.lock') + package = pypi.UvLockHandler.parse(test_file) + expected_loc = self.get_test_loc( + 'pypi/uv/attrs-uv.lock-expected.json', + must_exist=False, + ) + self.check_packages_data( + package, expected_loc, must_exist=False, regen=REGEN_TEST_FIXTURES, + ) + + def test_package_scan_uv_end_to_end(self): + test_dir = self.get_test_loc('pypi/uv/attrs/') + result_file = self.get_temp_file('json') + expected_file = self.get_test_loc( + 'pypi/uv/attrs-package-assembly-expected.json', + must_exist=False, + ) + run_scan_click(['--package', '--processes', '-1', test_dir, '--json-pp', result_file]) + check_json_scan(expected_file, result_file, remove_uuid=True, regen=REGEN_TEST_FIXTURES) + + class TestPipInspectDeplockHandler(PackageTester): test_data_dir = os.path.join(os.path.dirname(__file__), 'data')