Skip to content
Snippets Groups Projects
Unverified Commit be8b5a8a authored by Marc Mueller's avatar Marc Mueller Committed by GitHub
Browse files

Add option to extract licenses [ci] (#129095)

parent 99ed39b2
No related branches found
No related tags found
No related merge requests found
...@@ -615,6 +615,10 @@ jobs: ...@@ -615,6 +615,10 @@ jobs:
&& github.event.inputs.mypy-only != 'true' && github.event.inputs.mypy-only != 'true'
|| github.event.inputs.audit-licenses-only == 'true') || github.event.inputs.audit-licenses-only == 'true')
&& needs.info.outputs.requirements == 'true' && needs.info.outputs.requirements == 'true'
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
...@@ -633,19 +637,19 @@ jobs: ...@@ -633,19 +637,19 @@ jobs:
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
- name: Run pip-licenses - name: Extract license data
run: | run: |
. venv/bin/activate . venv/bin/activate
pip-licenses --format=json --output-file=licenses.json python -m script.licenses extract --output-file=licenses-${{ matrix.python-version }}.json
- name: Upload licenses - name: Upload licenses
uses: actions/upload-artifact@v4.4.3 uses: actions/upload-artifact@v4.4.3
with: with:
name: licenses name: licenses-${{ github.run_number }}-${{ matrix.python-version }}
path: licenses.json path: licenses-${{ matrix.python-version }}.json
- name: Process licenses - name: Check licenses
run: | run: |
. venv/bin/activate . venv/bin/activate
python -m script.licenses licenses.json python -m script.licenses check licenses-${{ matrix.python-version }}.json
pylint: pylint:
name: Check pylint name: Check pylint
......
...@@ -17,7 +17,6 @@ pydantic==1.10.18 ...@@ -17,7 +17,6 @@ pydantic==1.10.18
pylint==3.3.1 pylint==3.3.1
pylint-per-file-ignores==1.3.2 pylint-per-file-ignores==1.3.2
pipdeptree==2.23.4 pipdeptree==2.23.4
pip-licenses==5.0.0
pytest-asyncio==0.24.0 pytest-asyncio==0.24.0
pytest-aiohttp==1.0.5 pytest-aiohttp==1.0.5
pytest-cov==5.0.0 pytest-cov==5.0.0
......
...@@ -2,16 +2,28 @@ ...@@ -2,16 +2,28 @@
from __future__ import annotations from __future__ import annotations
from argparse import ArgumentParser from argparse import ArgumentParser, Namespace
from collections.abc import Sequence from collections.abc import Sequence
from dataclasses import dataclass from dataclasses import dataclass
from importlib import metadata
import json import json
from pathlib import Path from pathlib import Path
import sys import sys
from typing import TypedDict, cast
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
class PackageMetadata(TypedDict):
"""Package metadata."""
name: str
version: str
license_expression: str | None
license_metadata: str | None
license_classifier: list[str]
@dataclass @dataclass
class PackageDefinition: class PackageDefinition:
"""Package definition.""" """Package definition."""
...@@ -21,12 +33,16 @@ class PackageDefinition: ...@@ -21,12 +33,16 @@ class PackageDefinition:
version: AwesomeVersion version: AwesomeVersion
@classmethod @classmethod
def from_dict(cls, data: dict[str, str]) -> PackageDefinition: def from_dict(cls, data: PackageMetadata) -> PackageDefinition:
"""Create a package definition from a dictionary.""" """Create a package definition from PackageMetadata."""
if not (license_str := "; ".join(data["license_classifier"])):
license_str = (
data["license_metadata"] or data["license_expression"] or "UNKNOWN"
)
return cls( return cls(
license=data["License"], license=license_str,
name=data["Name"], name=data["name"],
version=AwesomeVersion(data["Version"]), version=AwesomeVersion(data["version"]),
) )
...@@ -128,7 +144,6 @@ EXCEPTIONS = { ...@@ -128,7 +144,6 @@ EXCEPTIONS = {
"aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180 "aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180
"aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94 "aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94
"aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8 "aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8
"aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6
"apple_weatherkit", # https://github.com/tjhorner/python-weatherkit/pull/3 "apple_weatherkit", # https://github.com/tjhorner/python-weatherkit/pull/3
"asyncio", # PSF License "asyncio", # PSF License
"chacha20poly1305", # LGPL "chacha20poly1305", # LGPL
...@@ -159,14 +174,10 @@ EXCEPTIONS = { ...@@ -159,14 +174,10 @@ EXCEPTIONS = {
"pyvera", # https://github.com/maximvelichko/pyvera/pull/164 "pyvera", # https://github.com/maximvelichko/pyvera/pull/164
"pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11 "pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11
"repoze.lru", "repoze.lru",
"ruuvitag-ble", # https://github.com/Bluetooth-Devices/ruuvitag-ble/pull/10
"sensirion-ble", # https://github.com/akx/sensirion-ble/pull/9
"sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14 "sharp_aquos_rc", # https://github.com/jmoore987/sharp_aquos_rc/pull/14
"tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5 "tapsaff", # https://github.com/bazwilliams/python-taps-aff/pull/5
"vincenty", # Public domain "vincenty", # Public domain
"zeversolar", # https://github.com/kvanzuijlen/zeversolar/pull/46 "zeversolar", # https://github.com/kvanzuijlen/zeversolar/pull/46
# Using License-Expression (with hatchling)
"ftfy", # Apache-2.0
} }
TODO = { TODO = {
...@@ -176,22 +187,9 @@ TODO = { ...@@ -176,22 +187,9 @@ TODO = {
} }
def main(argv: Sequence[str] | None = None) -> int: def check_licenses(args: CheckArgs) -> int:
"""Run the main script.""" """Check licenses are OSI approved."""
exit_code = 0 exit_code = 0
parser = ArgumentParser()
parser.add_argument(
"path",
nargs="?",
metavar="PATH",
default="licenses.json",
help="Path to json licenses file",
)
argv = argv or sys.argv[1:]
args = parser.parse_args(argv)
raw_licenses = json.loads(Path(args.path).read_text()) raw_licenses = json.loads(Path(args.path).read_text())
package_definitions = [PackageDefinition.from_dict(data) for data in raw_licenses] package_definitions = [PackageDefinition.from_dict(data) for data in raw_licenses]
for package in package_definitions: for package in package_definitions:
...@@ -244,8 +242,92 @@ def main(argv: Sequence[str] | None = None) -> int: ...@@ -244,8 +242,92 @@ def main(argv: Sequence[str] | None = None) -> int:
return exit_code return exit_code
def extract_licenses(args: ExtractArgs) -> int:
"""Extract license data for installed packages."""
licenses = sorted(
[get_package_metadata(dist) for dist in list(metadata.distributions())],
key=lambda dist: dist["name"],
)
Path(args.output_file).write_text(json.dumps(licenses, indent=2))
return 0
def get_package_metadata(dist: metadata.Distribution) -> PackageMetadata:
"""Get package metadata for distribution."""
return {
"name": dist.name,
"version": dist.version,
"license_expression": dist.metadata.get("License-Expression"),
"license_metadata": dist.metadata.get("License"),
"license_classifier": extract_license_classifier(
dist.metadata.get_all("Classifier")
),
}
def extract_license_classifier(classifiers: list[str] | None) -> list[str]:
"""Extract license from list of classifiers.
E.g. 'License :: OSI Approved :: MIT License' -> 'MIT License'.
Filter out bare 'License :: OSI Approved'.
"""
return [
license_classifier
for classifier in classifiers or ()
if classifier.startswith("License")
and (license_classifier := classifier.rpartition(" :: ")[2])
and license_classifier != "OSI Approved"
]
class ExtractArgs(Namespace):
"""Extract arguments."""
output_file: str
class CheckArgs(Namespace):
"""Check arguments."""
path: str
def main(argv: Sequence[str] | None = None) -> int:
"""Run the main script."""
parser = ArgumentParser()
subparsers = parser.add_subparsers(title="Subcommands", required=True)
parser_extract = subparsers.add_parser("extract")
parser_extract.set_defaults(action="extract")
parser_extract.add_argument(
"--output-file",
default="licenses.json",
help="Path to store the licenses file",
)
parser_check = subparsers.add_parser("check")
parser_check.set_defaults(action="check")
parser_check.add_argument(
"path",
nargs="?",
metavar="PATH",
default="licenses.json",
help="Path to json licenses file",
)
argv = argv or sys.argv[1:]
args = parser.parse_args(argv)
if args.action == "extract":
args = cast(ExtractArgs, args)
return extract_licenses(args)
if args.action == "check":
args = cast(CheckArgs, args)
if (exit_code := check_licenses(args)) == 0:
print("All licenses are approved!")
return exit_code
return 0
if __name__ == "__main__": if __name__ == "__main__":
exit_code = main() sys.exit(main())
if exit_code == 0:
print("All licenses are approved!")
sys.exit(exit_code)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment